Skip to content

jwskate.jwk

This module implements Json Web Key RFC7517.

ExpectedAlgRequired

Bases: ValueError

Raised when the expected signature alg(s) must be provided.

Source code in jwskate/jwk/alg.py
15
16
class ExpectedAlgRequired(ValueError):
    """Raised when the expected signature alg(s) must be provided."""

MismatchingAlg

Bases: ValueError

Raised when attempting a cryptographic operation with an unexpected algorithm.

Signature verification or a decryption operation with an algorithm that does not match the algorithm specified in the key or the token.

Source code in jwskate/jwk/alg.py
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class MismatchingAlg(ValueError):
    """Raised when attempting a cryptographic operation with an unexpected algorithm.

    Signature verification or a decryption operation with an algorithm that does not match the
    algorithm specified in the key or the token.

    """

    def __init__(
        self,
        target_alg: str,
        alg: str | None = None,
        algs: Iterable[str] | None = None,
    ) -> None:
        self.target_alg = target_alg
        self.alg = alg
        self.algs = list(algs) if algs else None

UnsupportedAlg

Bases: ValueError

Raised when a unsupported alg is requested.

Source code in jwskate/jwk/alg.py
11
12
class UnsupportedAlg(ValueError):
    """Raised when a unsupported alg is requested."""

InvalidJwk

Bases: ValueError

Raised when an invalid JWK is encountered.

Source code in jwskate/jwk/base.py
50
51
class InvalidJwk(ValueError):
    """Raised when an invalid JWK is encountered."""

Jwk

Bases: BaseJsonDict

Represents a Json Web Key (JWK), as specified in RFC7517.

A JWK is a JSON object that represents a cryptographic key. The members of the object represent properties of the key, also called parameters, which are name and value pairs.

Just like a parsed JSON object, a Jwk is a dict, so you can do with a Jwk anything you can do with a dict. In addition, all keys parameters are exposed as attributes. There are subclasses of Jwk for each specific Key Type, but unless you are dealing with specific parameters for a given key type, you shouldn't have to use the subclasses directly since they all present a common interface for cryptographic operations.

Parameters:

Name Type Description Default
params Mapping[str, Any] | Any

one of

  • a dict parsed from a JWK
  • a JSON JWK
  • a cryptography key
  • another Jwk
  • a str containing the JSON representation of a JWK
  • a raw bytes
required
include_kid_thumbprint bool

if True, and there is no kid in the provided params, generate a kid based on the key thumbprint. Default to False. DEPRECATED: Use with_kid_thumbprint().

False
Source code in jwskate/jwk/base.py
  64
  65
  66
  67
  68
  69
  70
  71
  72
  73
  74
  75
  76
  77
  78
  79
  80
  81
  82
  83
  84
  85
  86
  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
 132
 133
 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
 173
 174
 175
 176
 177
 178
 179
 180
 181
 182
 183
 184
 185
 186
 187
 188
 189
 190
 191
 192
 193
 194
 195
 196
 197
 198
 199
 200
 201
 202
 203
 204
 205
 206
 207
 208
 209
 210
 211
 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
 240
 241
 242
 243
 244
 245
 246
 247
 248
 249
 250
 251
 252
 253
 254
 255
 256
 257
 258
 259
 260
 261
 262
 263
 264
 265
 266
 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
 293
 294
 295
 296
 297
 298
 299
 300
 301
 302
 303
 304
 305
 306
 307
 308
 309
 310
 311
 312
 313
 314
 315
 316
 317
 318
 319
 320
 321
 322
 323
 324
 325
 326
 327
 328
 329
 330
 331
 332
 333
 334
 335
 336
 337
 338
 339
 340
 341
 342
 343
 344
 345
 346
 347
 348
 349
 350
 351
 352
 353
 354
 355
 356
 357
 358
 359
 360
 361
 362
 363
 364
 365
 366
 367
 368
 369
 370
 371
 372
 373
 374
 375
 376
 377
 378
 379
 380
 381
 382
 383
 384
 385
 386
 387
 388
 389
 390
 391
 392
 393
 394
 395
 396
 397
 398
 399
 400
 401
 402
 403
 404
 405
 406
 407
 408
 409
 410
 411
 412
 413
 414
 415
 416
 417
 418
 419
 420
 421
 422
 423
 424
 425
 426
 427
 428
 429
 430
 431
 432
 433
 434
 435
 436
 437
 438
 439
 440
 441
 442
 443
 444
 445
 446
 447
 448
 449
 450
 451
 452
 453
 454
 455
 456
 457
 458
 459
 460
 461
 462
 463
 464
 465
 466
 467
 468
 469
 470
 471
 472
 473
 474
 475
 476
 477
 478
 479
 480
 481
 482
 483
 484
 485
 486
 487
 488
 489
 490
 491
 492
 493
 494
 495
 496
 497
 498
 499
 500
 501
 502
 503
 504
 505
 506
 507
 508
 509
 510
 511
 512
 513
 514
 515
 516
 517
 518
 519
 520
 521
 522
 523
 524
 525
 526
 527
 528
 529
 530
 531
 532
 533
 534
 535
 536
 537
 538
 539
 540
 541
 542
 543
 544
 545
 546
 547
 548
 549
 550
 551
 552
 553
 554
 555
 556
 557
 558
 559
 560
 561
 562
 563
 564
 565
 566
 567
 568
 569
 570
 571
 572
 573
 574
 575
 576
 577
 578
 579
 580
 581
 582
 583
 584
 585
 586
 587
 588
 589
 590
 591
 592
 593
 594
 595
 596
 597
 598
 599
 600
 601
 602
 603
 604
 605
 606
 607
 608
 609
 610
 611
 612
 613
 614
 615
 616
 617
 618
 619
 620
 621
 622
 623
 624
 625
 626
 627
 628
 629
 630
 631
 632
 633
 634
 635
 636
 637
 638
 639
 640
 641
 642
 643
 644
 645
 646
 647
 648
 649
 650
 651
 652
 653
 654
 655
 656
 657
 658
 659
 660
 661
 662
 663
 664
 665
 666
 667
 668
 669
 670
 671
 672
 673
 674
 675
 676
 677
 678
 679
 680
 681
 682
 683
 684
 685
 686
 687
 688
 689
 690
 691
 692
 693
 694
 695
 696
 697
 698
 699
 700
 701
 702
 703
 704
 705
 706
 707
 708
 709
 710
 711
 712
 713
 714
 715
 716
 717
 718
 719
 720
 721
 722
 723
 724
 725
 726
 727
 728
 729
 730
 731
 732
 733
 734
 735
 736
 737
 738
 739
 740
 741
 742
 743
 744
 745
 746
 747
 748
 749
 750
 751
 752
 753
 754
 755
 756
 757
 758
 759
 760
 761
 762
 763
 764
 765
 766
 767
 768
 769
 770
 771
 772
 773
 774
 775
 776
 777
 778
 779
 780
 781
 782
 783
 784
 785
 786
 787
 788
 789
 790
 791
 792
 793
 794
 795
 796
 797
 798
 799
 800
 801
 802
 803
 804
 805
 806
 807
 808
 809
 810
 811
 812
 813
 814
 815
 816
 817
 818
 819
 820
 821
 822
 823
 824
 825
 826
 827
 828
 829
 830
 831
 832
 833
 834
 835
 836
 837
 838
 839
 840
 841
 842
 843
 844
 845
 846
 847
 848
 849
 850
 851
 852
 853
 854
 855
 856
 857
 858
 859
 860
 861
 862
 863
 864
 865
 866
 867
 868
 869
 870
 871
 872
 873
 874
 875
 876
 877
 878
 879
 880
 881
 882
 883
 884
 885
 886
 887
 888
 889
 890
 891
 892
 893
 894
 895
 896
 897
 898
 899
 900
 901
 902
 903
 904
 905
 906
 907
 908
 909
 910
 911
 912
 913
 914
 915
 916
 917
 918
 919
 920
 921
 922
 923
 924
 925
 926
 927
 928
 929
 930
 931
 932
 933
 934
 935
 936
 937
 938
 939
 940
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
class Jwk(BaseJsonDict):
    """Represents a Json Web Key (JWK), as specified in RFC7517.

    A JWK is a JSON object that represents a cryptographic key.  The
    members of the object represent properties of the key, also called
    parameters, which are name and value pairs.

    Just like a parsed JSON object, a `Jwk` is a `dict`, so
    you can do with a `Jwk` anything you can do with a `dict`. In
    addition, all keys parameters are exposed as attributes. There are
    subclasses of `Jwk` for each specific Key Type, but unless you are
    dealing with specific parameters for a given key type, you shouldn't
    have to use the subclasses directly since they all present a common
    interface for cryptographic operations.

    Args:
        params: one of

            - a `dict` parsed from a JWK
            - a JSON JWK
            - a `cryptography key`
            - another `Jwk`
            - a `str` containing the JSON representation of a JWK
            - a raw `bytes`
        include_kid_thumbprint: if `True`, and there is no `kid` in the provided params,
            generate a kid based on the key thumbprint. Default to `False`.
            *DEPRECATED: Use `with_kid_thumbprint()`.*

    """

    @classmethod
    def generate_for_alg(cls, alg: str, **kwargs: Any) -> Jwk:
        """Generate a key for usage with a specific `alg` and return the resulting `Jwk`.

        Args:
            alg: a signature or key management algorithm identifier
            **kwargs: specific parameters, depending on the key type, or additional members to include in the `Jwk`

        Returns:
            the generated `Jwk`

        """
        for jwk_class in Jwk.__subclasses__():
            try:
                jwk_class._get_alg_class(alg)
                return jwk_class.generate(alg=alg, **kwargs)
            except UnsupportedAlg:
                continue

        raise UnsupportedAlg(alg)

    @classmethod
    def generate_for_kty(cls, kty: str, **kwargs: Any) -> Jwk:
        """Generate a key with a specific type and return the resulting `Jwk`.

        Args:
          kty: key type to generate
          **kwargs: specific parameters depending on the key type, or additional members to include in the `Jwk`

        Returns:
            the resulting `Jwk`

        Raises:
            UnsupportedKeyType: if the specified key type (`kty`) is not supported

        """
        for jwk_class in Jwk.__subclasses__():
            if kty == jwk_class.KTY:
                return jwk_class.generate(**kwargs)
        msg = "Unsupported Key Type:"
        raise UnsupportedKeyType(msg, kty)

    PARAMS: Mapping[str, JwkParameter]

    KTY: ClassVar[str]

    CRYPTOGRAPHY_PRIVATE_KEY_CLASSES: ClassVar[tuple[type[Any], ...]]
    CRYPTOGRAPHY_PUBLIC_KEY_CLASSES: ClassVar[tuple[type[Any], ...]]

    SIGNATURE_ALGORITHMS: Mapping[str, type[BaseSignatureAlg]] = {}
    KEY_MANAGEMENT_ALGORITHMS: Mapping[str, type[BaseKeyManagementAlg]] = {}
    ENCRYPTION_ALGORITHMS: Mapping[str, type[BaseAESEncryptionAlg]] = {}

    IANA_HASH_FUNCTION_NAMES: Mapping[str, str] = {
        # IANA registered names to binapy hash name
        "sha-1": "sha1",
        "sha-224": "sha224",
        "sha-256": "sha256",
        "sha-384": "sha384",
        "sha-512": "sha512",
        "shake128": "shake128",
        "shake256": "shake256",
    }

    def __new__(cls, key: Jwk | Mapping[str, Any] | Any, **kwargs: Any) -> Jwk:
        """Overridden `__new__` to make the Jwk constructor smarter.

        The `Jwk` constructor will accept:

            - a `dict` with the parsed Jwk content
            - another `Jwk`, which will be used as-is instead of creating a copy
            - an instance from a `cryptography` public or private key class

        Args:
            key: the source for key materials
            **kwargs: additional members to include in the Jwk

        """
        if cls == Jwk:
            if isinstance(key, Jwk):
                return cls.from_cryptography_key(key.cryptography_key, **kwargs)
            if isinstance(key, Mapping):
                kty: str | None = key.get("kty")
                if kty is None:
                    msg = "A Json Web Key must have a Key Type (kty)"
                    raise InvalidJwk(msg)

                for jwk_class in Jwk.__subclasses__():
                    if kty == jwk_class.KTY:
                        return super().__new__(jwk_class)

                msg = "Unsupported Key Type"
                raise InvalidJwk(msg, kty)

            elif isinstance(key, str):
                return cls.from_json(key)
            else:
                return cls.from_cryptography_key(key, **kwargs)
        return super().__new__(cls)

    def __init__(self, params: Mapping[str, Any] | Any, *, include_kid_thumbprint: bool = False):
        if isinstance(params, dict):  # this is to avoid double init due to the __new__ above
            super().__init__({key: val for key, val in params.items() if val is not None})
            self._validate()
            if self.get("kid") is None and include_kid_thumbprint:
                self["kid"] = self.thumbprint()

        try:
            self.cryptography_key = self._to_cryptography_key()
        except Exception as exc:
            raise InvalidJwk(params) from exc

    @classmethod
    def _get_alg_class(cls, alg: str) -> type[BaseAlg]:
        """Given an alg identifier, return the matching JWA wrapper.

        Args:
            alg: an alg identifier

        Returns:
            the matching JWA wrapper

        """
        alg_class: type[BaseAlg] | None

        alg_class = cls.SIGNATURE_ALGORITHMS.get(alg)
        if alg_class is not None:
            return alg_class

        alg_class = cls.KEY_MANAGEMENT_ALGORITHMS.get(alg)
        if alg_class is not None:
            return alg_class

        alg_class = cls.ENCRYPTION_ALGORITHMS.get(alg)
        if alg_class is not None:
            return alg_class

        raise UnsupportedAlg(alg)

    @property
    def is_private(self) -> bool:
        """Return `True` if the key is private, `False` otherwise.

        Returns:
            `True` if the key is private, `False` otherwise

        """
        return True

    @property
    def is_symmetric(self) -> bool:
        """Return `True` if the key is symmetric, `False` otherwise."""
        return False

    def __getattr__(self, param: str) -> Any:
        """Allow access to key parameters as attributes.

        This is a convenience to allow `jwk.param` instead of `jwk['param']`.

        Args:
            param: the parameter name to access

        Return:
            the param value

        Raises:
            AttributeError: if the param is not found

        """
        value = self.get(param)
        if value is None:
            raise AttributeError(param)
        return value

    def __setitem__(self, key: str, value: Any) -> None:
        """Override base method to avoid modifying cryptographic key attributes.

        Args:
            key: name of the attribute to set
            value: value to set

        Raises:
            RuntimeError: when trying to modify cryptographic attributes

        """
        # don't allow modifying private attributes after the key has been initialized
        if key in self.PARAMS and hasattr(self, "cryptography_key"):
            msg = "JWK key attributes cannot be modified."
            raise RuntimeError(msg)
        super().__setitem__(key, value)

    @property
    def kty(self) -> str:
        """Return the Key Type.

        Returns:
            the key type

        """
        return self.KTY

    @property
    def alg(self) -> str | None:
        """Return the configured key alg, if any.

        Returns:
            the key alg

        """
        alg = self.get("alg")
        if alg is not None and not isinstance(alg, str):  # pragma: no branch
            msg = f"Invalid alg type {type(alg)}"
            raise TypeError(msg, alg)
        return alg

    @property
    def kid(self) -> str:
        """Return the JWK key ID (kid).

        If the kid is not explicitly set, the RFC7638 key thumbprint is returned.

        """
        kid = self.get("kid")
        if kid is not None and not isinstance(kid, str):  # pragma: no branch
            msg = f"invalid kid type {type(kid)}"
            raise TypeError(msg, kid)
        if kid is None:
            return self.thumbprint()
        return kid

    @property
    def use(self) -> str | None:
        """Return the key use.

        If no `alg` parameter is present, this returns the `use` parameter from this JWK. If an
        `alg` parameter is present, the use is deduced from this alg. To check for the presence of
        the `use` parameter, use `jwk.get('use')`.

        """
        if self.alg:
            return self._get_alg_class(self.alg).use
        else:
            return self.get("use")

    @property
    def key_ops(self) -> tuple[str, ...]:
        """Return the key operations.

        If no `alg` parameter is present, this returns the `key_ops` parameter from this JWK. If an
        `alg` parameter is present, the key operations are deduced from this alg. To check for the
        presence of the `key_ops` parameter, use `jwk.get('key_ops')`.

        """
        key_ops: tuple[str, ...]
        if self.use == "sig":
            if self.is_symmetric:
                key_ops = ("sign", "verify")
            elif self.is_private:
                key_ops = ("sign",)
            else:
                key_ops = ("verify",)
        elif self.use == "enc":
            if self.is_symmetric:
                if self.alg:
                    alg_class = self._get_alg_class(self.alg)
                    if issubclass(alg_class, BaseKeyManagementAlg):
                        key_ops = ("wrapKey", "unwrapKey")
                    elif issubclass(alg_class, BaseAESEncryptionAlg):
                        key_ops = ("encrypt", "decrypt")
                else:
                    key_ops = ("wrapKey", "unwrapKey", "encrypt", "decrypt")
            elif self.is_private:
                key_ops = ("unwrapKey",)
            else:
                key_ops = ("wrapKey",)
        else:
            key_ops = self.get("key_ops", ())

        return tuple(key_ops)

    def thumbprint(self, hashalg: str = "sha-256") -> str:
        """Return the key thumbprint as specified by RFC 7638.

        Args:
          hashalg: A hash function (defaults to SHA256)

        Returns:
            the calculated thumbprint

        """
        alg = self.IANA_HASH_FUNCTION_NAMES.get(hashalg)
        if not alg:
            msg = f"Unsupported hash alg {hashalg}"
            raise ValueError(msg)

        t = {"kty": self.get("kty")}
        for name, param in self.PARAMS.items():
            if param.is_required and not param.is_private:
                t[name] = self.get(name)

        return BinaPy.serialize_to("json", t, separators=(",", ":"), sort_keys=True).to(alg).to("b64u").ascii()

    def thumbprint_uri(self, hashalg: str = "sha-256") -> str:
        """Return the JWK thumbprint URI for this key.

        Args:
            hashalg: the IANA registered name for the hash alg to use

        Returns:
             the JWK thumbprint URI for this `Jwk`

        """
        thumbprint = self.thumbprint(hashalg)
        return f"urn:ietf:params:oauth:jwk-thumbprint:{hashalg}:{thumbprint}"

    def check(
        self,
        *,
        is_private: bool | None = None,
        is_symmetric: bool | None = None,
        kty: str | None = None,
    ) -> Jwk:
        """Check this key for type, privateness and/or symmetricness.

        This raises a `ValueError` if the key is not as expected.

        Args:
            is_private:

                - if `True`, check if the key is private,
                - if `False`, check if it is public,
                - if `None`, do nothing
            is_symmetric:

                - if `True`, check if the key is symmetric,
                - if `False`, check if it is asymmetric,
                - if `None`, do nothing
            kty: the expected key type, if any

        Returns:
            this key, if all checks passed

        Raises:
            ValueError: if any check fails

        """
        if is_private is not None:
            if is_private and not self.is_private:
                msg = "This key is public while a private key is expected."
                raise ValueError(msg)
            elif not is_private and self.is_private:
                msg = "This key is private while a public key is expected."
                raise ValueError(msg)

        if is_symmetric is not None:
            if is_symmetric and not self.is_symmetric:
                msg = "This key is asymmetric while a symmetric key is expected."
                raise ValueError(msg)
            if not is_symmetric and self.is_symmetric:
                msg = "This key is symmetric while an asymmetric key is expected."
                raise ValueError(msg)

        if kty is not None and self.kty != kty:
            msg = f"This key has kty={self.kty} while a kty={kty} is expected."
            raise ValueError(msg)

        return self

    def _validate(self) -> None:  # noqa: C901
        """Validate the content of this `Jwk`.

        It checks that all required parameters are present and well-formed.
        If the key is private, it sets the `is_private` flag to `True`.

        Raises:
            TypeError: if the key type doesn't match the subclass
            InvalidJwk: if the JWK misses required members or has invalid members

        """
        if self.get("kty") != self.KTY:
            msg = f"This key 'kty' {self.get('kty')} doesn't match this Jwk subclass intended 'kty' {self.KTY}!"
            raise TypeError(msg)

        jwk_is_private = False
        for name, param in self.PARAMS.items():
            value = self.get(name)

            if param.is_private and value is not None:
                jwk_is_private = True

            if not param.is_private and param.is_required and value is None:
                msg = f"Missing required public param {param.description} ({name})"
                raise InvalidJwk(msg)

            if value is None:
                pass
            elif param.kind == "b64u":
                if not isinstance(value, str):
                    msg = f"Parameter {param.description} ({name}) must be a string with a Base64URL-encoded value"
                    raise InvalidJwk(msg)
                if not BinaPy(value).check("b64u"):
                    msg = f"Parameter {param.description} ({name}) must be a Base64URL-encoded value"
                    raise InvalidJwk(msg)
            elif param.kind == "unsupported":
                if value is not None:  # pragma: no cover
                    msg = f"Unsupported JWK param '{name}'"
                    raise InvalidJwk(msg)
            elif param.kind == "name":
                pass
            else:
                msg = f"Unsupported param '{name}' type '{param.kind}'"
                raise AssertionError(msg)  # pragma: no cover

        # if at least one of the supplied parameter was private, then all required private parameters must be provided
        if jwk_is_private:
            for name, param in self.PARAMS.items():
                value = self.get(name)
                if param.is_private and param.is_required and value is None:
                    msg = f"Missing required private param {param.description} ({name})"
                    raise InvalidJwk(msg)

        # if key is used for signing, it must be private
        for op in self.get("key_ops", []):
            if op in ("sign", "unwrapKey") and not self.is_private:
                msg = f"Key Operation is '{op}' but the key is public"
                raise InvalidJwk(msg)

    def signature_class(self, alg: str | None = None) -> type[BaseSignatureAlg]:
        """Return the appropriate signature algorithm class to use with this key.

        The returned class is a `BaseSignatureAlg` subclass.

        If this key doesn't have an `alg` parameter, you must supply one as parameter to this method.

        Args:
            alg: the algorithm identifier, if not already present in this Jwk

        Returns:
            the appropriate `BaseSignatureAlg` subclass

        """
        return select_alg_class(self.SIGNATURE_ALGORITHMS, jwk_alg=self.alg, alg=alg)

    def encryption_class(self, alg: str | None = None) -> type[BaseAESEncryptionAlg]:
        """Return the appropriate encryption algorithm class to use with this key.

        The returned class is a subclass of `BaseAESEncryptionAlg`.

        If this key doesn't have an `alg` parameter, you must supply one as parameter to this method.

        Args:
            alg: the algorithm identifier, if not already present in this Jwk

        Returns:
            the appropriate `BaseAESEncryptionAlg` subclass

        """
        return select_alg_class(self.ENCRYPTION_ALGORITHMS, jwk_alg=self.alg, alg=alg)

    def key_management_class(self, alg: str | None = None) -> type[BaseKeyManagementAlg]:
        """Return the appropriate key management algorithm class to use with this key.

        If this key doesn't have an `alg` parameter, you must supply one as parameter to this method.

        Args:
            alg: the algorithm identifier, if not already present in this Jwk

        Returns:
            the appropriate `BaseKeyManagementAlg` subclass

        """
        return select_alg_class(self.KEY_MANAGEMENT_ALGORITHMS, jwk_alg=self.alg, alg=alg)

    def signature_wrapper(self, alg: str | None = None) -> BaseSignatureAlg:
        """Initialize a  key management wrapper with this key.

        This returns an instance of a `BaseSignatureAlg` subclass.

        If this key doesn't have an `alg` parameter, you must supply one as parameter to this method.

        Args:
            alg: the algorithm identifier, if not already present in this Jwk

        Returns:
            a `BaseSignatureAlg` instance initialized with the current key

        """
        alg_class = self.signature_class(alg)
        if issubclass(alg_class, BaseSymmetricAlg):
            return alg_class(self.key)
        elif issubclass(alg_class, BaseAsymmetricAlg):
            return alg_class(self.cryptography_key)
        raise UnsupportedAlg(alg)  # pragma: no cover

    def encryption_wrapper(self, alg: str | None = None) -> BaseAESEncryptionAlg:
        """Initialize an encryption wrapper with this key.

        This returns an instance of a `BaseAESEncryptionAlg` subclass.

        If this key doesn't have an `alg` parameter, you must supply one as parameter to this method.

        Args:
            alg: the algorithm identifier, if not already present in this Jwk

        Returns:
            a `BaseAESEncryptionAlg` instance initialized with the current key

        """
        alg_class = self.encryption_class(alg)
        if issubclass(alg_class, BaseSymmetricAlg):
            return alg_class(self.key)
        elif issubclass(alg_class, BaseAsymmetricAlg):  # pragma: no cover
            return alg_class(self.cryptography_key)  # pragma: no cover
        raise UnsupportedAlg(alg)  # pragma: no cover

    def key_management_wrapper(self, alg: str | None = None) -> BaseKeyManagementAlg:
        """Initialize a key management wrapper with this key.

        This returns an instance of a `BaseKeyManagementAlg` subclass.

        If this key doesn't have an `alg` parameter, you must supply one as parameter to this method.

        Args:
            alg: the algorithm identifier, if not already present in this Jwk

        Returns:
            a `BaseKeyManagementAlg` instance initialized with the current key

        """
        alg_class = self.key_management_class(alg)
        if issubclass(alg_class, BaseSymmetricAlg):
            return alg_class(self.key)
        elif issubclass(alg_class, BaseAsymmetricAlg):
            return alg_class(self.cryptography_key)
        raise UnsupportedAlg(alg)  # pragma: no cover

    def supported_signing_algorithms(self) -> list[str]:
        """Return the list of Signature algorithms that can be used with this key.

        Returns:
          a list of supported algs

        """
        return list(self.SIGNATURE_ALGORITHMS)

    def supported_key_management_algorithms(self) -> list[str]:
        """Return the list of Key Management algorithms that can be used with this key.

        Returns:
            a list of supported algs

        """
        return list(self.KEY_MANAGEMENT_ALGORITHMS)

    def supported_encryption_algorithms(self) -> list[str]:
        """Return the list of Encryption algorithms that can be used with this key.

        Returns:
            a list of supported algs

        """
        return list(self.ENCRYPTION_ALGORITHMS)

    def sign(self, data: bytes | SupportsBytes, alg: str | None = None) -> BinaPy:
        """Sign data using this Jwk, and return the generated signature.

        Args:
          data: the data to sign
          alg: the alg to use (if this key doesn't have an `alg` parameter)

        Returns:
          the generated signature

        """
        wrapper = self.signature_wrapper(alg)
        signature = wrapper.sign(data)
        return BinaPy(signature)

    def verify(
        self,
        data: bytes | SupportsBytes,
        signature: bytes | SupportsBytes,
        *,
        alg: str | None = None,
        algs: Iterable[str] | None = None,
    ) -> bool:
        """Verify a signature using this `Jwk`, and return `True` if valid.

        Args:
          data: the data to verify
          signature: the signature to verify
          alg: the allowed signature alg, if there is only one
          algs: the allowed signature algs, if there are several

        Returns:
          `True` if the signature matches, `False` otherwise

        """
        if not self.is_symmetric and self.is_private:
            warnings.warn(
                "You are trying to validate a signature with a private key. "
                "Signatures should always be verified with a public key.",
                stacklevel=2,
            )
            public_jwk = self.public_jwk()
        else:
            public_jwk = self
        if algs is None and alg:
            algs = [alg]
        for alg in algs or (None,):
            wrapper = public_jwk.signature_wrapper(alg)
            if wrapper.verify(data, signature):
                return True

        return False

    def encrypt(
        self,
        plaintext: bytes | SupportsBytes,
        *,
        aad: bytes | None = None,
        alg: str | None = None,
        iv: bytes | None = None,
    ) -> tuple[BinaPy, BinaPy, BinaPy]:
        """Encrypt a plaintext with Authenticated Encryption using this key.

        Authenticated Encryption with Associated Data (AEAD) is supported,
        by passing Additional Authenticated Data (`aad`).

        This returns a tuple with 3 raw data, in order:
        - the encrypted Data
        - the Initialization Vector that was used to encrypt data
        - the generated Authentication Tag

        Args:
          plaintext: the data to encrypt.
          aad: the Additional Authenticated Data (AAD) to include in the authentication tag
          alg: the alg to use to encrypt the data
          iv: the Initialization Vector to use. If `None`, an IV is randomly generated.
              If a value is provided, the returned IV will be that same value. You should never reuse the same IV!

        Returns:
          a tuple (ciphertext, iv, authentication_tag), as raw data

        """
        raise NotImplementedError  # pragma: no cover

    def decrypt(
        self,
        ciphertext: bytes | SupportsBytes,
        *,
        iv: bytes | SupportsBytes,
        tag: bytes | SupportsBytes,
        aad: bytes | SupportsBytes | None = None,
        alg: str | None = None,
    ) -> BinaPy:
        """Decrypt an encrypted data using this Jwk, and return the encrypted result.

        This is implemented by subclasses.

        Args:
          ciphertext: the data to decrypt
          iv: the Initialization Vector (IV) that was used for encryption
          tag: the Authentication Tag that will be verified while decrypting data
          aad: the Additional Authentication Data (AAD) to verify the Tag against
          alg: the alg to use for decryption

        Returns:
          the clear-text data

        """
        raise NotImplementedError  # pragma: no cover

    def sender_key(  # noqa: C901
        self,
        enc: str,
        *,
        alg: str | None = None,
        cek: bytes | None = None,
        epk: Jwk | None = None,
        **headers: Any,
    ) -> tuple[Jwk, BinaPy, Mapping[str, Any]]:
        """Produce a Content Encryption Key, to use for encryption.

        This method is meant to be used by encrypted token senders.
        Recipients should use the matching method `Jwk.recipient_key()`.

        Returns a tuple with 3 items:

        - the clear text CEK, as a SymmetricJwk instance.
        Use this key to encrypt your message, but do not communicate this key to anyone!
        - the encrypted CEK, as bytes. You must send this to your recipient.
        This may be `None` for Key Management algs which derive a CEK instead of generating one.
        - extra headers depending on the Key Management algorithm, as a dict of name to values.
        You must send those to your recipient as well.

        For algorithms that rely on a randomly generated CEK, such as RSAES or AES, you can provide that CEK instead
        of letting `jwskate` generate a safe, unique random value for you.
        Likewise, for algorithms that rely on an ephemeral key, you can provide an EPK that you generated yourself,
        instead of letting `jwskate` generate an appropriate value for you.
        Only do this if you know what you are doing!

        Args:
          enc: the encryption algorithm to use with the CEK
          alg: the Key Management algorithm to use to produce the CEK
          cek: CEK to use (leave `None` to have an adequate random value generated automatically)
          epk: EPK to use (leave `None` to have an adequate ephemeral key generated automatically)
          **headers: additional headers to include for the CEK derivation

        Returns:
          a tuple (cek, wrapped_cek, additional_headers_map)

        Raises:
            UnsupportedAlg: if the requested alg identifier is not supported

        """
        from jwskate import SymmetricJwk

        if not self.is_symmetric and self.is_private:
            warnings.warn(
                "You are using a private key for sender key wrapping. "
                "Key wrapping should always be done using the recipient public key.",
                stacklevel=2,
            )
            key_alg_wrapper = self.public_jwk().key_management_wrapper(alg)
        else:
            key_alg_wrapper = self.key_management_wrapper(alg)

        enc_alg_class = select_alg_class(SymmetricJwk.ENCRYPTION_ALGORITHMS, alg=enc)

        cek_headers: dict[str, Any] = {}

        if isinstance(key_alg_wrapper, BaseRsaKeyWrap):
            if cek:
                enc_alg_class.check_key(cek)
            else:
                cek = enc_alg_class.generate_key()
            wrapped_cek = key_alg_wrapper.wrap_key(cek)

        elif isinstance(key_alg_wrapper, EcdhEs):
            epk = epk or Jwk.from_cryptography_key(key_alg_wrapper.generate_ephemeral_key())
            cek_headers = {"epk": epk.public_jwk()}
            if isinstance(key_alg_wrapper, BaseEcdhEs_AesKw):
                if cek:
                    enc_alg_class.check_key(cek)
                else:
                    cek = enc_alg_class.generate_key()
                wrapped_cek = key_alg_wrapper.wrap_key_with_epk(
                    cek, epk.cryptography_key, alg=key_alg_wrapper.name, **headers
                )
            else:
                cek = key_alg_wrapper.sender_key(
                    epk.cryptography_key,
                    alg=enc_alg_class.name,
                    key_size=enc_alg_class.key_size,
                    **headers,
                )
                wrapped_cek = BinaPy(b"")

        elif isinstance(key_alg_wrapper, BaseAesKeyWrap):
            if cek:
                enc_alg_class.check_key(cek)
            else:
                cek = enc_alg_class.generate_key()
            wrapped_cek = key_alg_wrapper.wrap_key(cek)

        elif isinstance(key_alg_wrapper, BaseAesGcmKeyWrap):
            if cek:
                enc_alg_class.check_key(cek)
            else:
                cek = enc_alg_class.generate_key()
            iv = key_alg_wrapper.generate_iv()
            wrapped_cek, tag = key_alg_wrapper.wrap_key(cek, iv=iv)
            cek_headers = {
                "iv": iv.to("b64u").ascii(),
                "tag": tag.to("b64u").ascii(),
            }

        elif isinstance(key_alg_wrapper, DirectKeyUse):
            cek = key_alg_wrapper.direct_key(enc_alg_class)
            wrapped_cek = BinaPy(b"")
        else:
            msg = f"Unsupported Key Management Alg {key_alg_wrapper}"
            raise UnsupportedAlg(msg)  # pragma: no cover

        return SymmetricJwk.from_bytes(cek), wrapped_cek, cek_headers

    def recipient_key(  # noqa: C901
        self,
        wrapped_cek: bytes | SupportsBytes,
        enc: str,
        *,
        alg: str | None = None,
        **headers: Any,
    ) -> Jwk:
        """Produce a Content Encryption Key, to use for decryption.

        This method is meant to be used by encrypted token recipient.
        Senders should use the matching method `Jwk.sender_key()`.

        Args:
          wrapped_cek: the wrapped CEK
          enc: the encryption algorithm to use with the CEK
          alg: the Key Management algorithm to use to unwrap the CEK
          **headers: additional headers used to decrypt the CEK (e.g. "epk" for ECDH algs, "iv", "tag" for AES-GCM algs)

        Returns:
          the clear-text CEK, as a SymmetricJwk instance

        Raises:
            UnsupportedAlg: if the requested alg identifier is not supported

        """
        from jwskate import SymmetricJwk

        if not self.is_symmetric and not self.is_private:
            msg = (
                "You are using a public key for recipient key unwrapping. "
                "Key unwrapping must always be done using the recipient private key."
            )
            raise ValueError(msg)

        key_alg_wrapper = self.key_management_wrapper(alg)
        enc_alg_class = select_alg_class(SymmetricJwk.ENCRYPTION_ALGORITHMS, alg=enc)

        if isinstance(key_alg_wrapper, BaseRsaKeyWrap):
            cek = key_alg_wrapper.unwrap_key(wrapped_cek)

        elif isinstance(key_alg_wrapper, EcdhEs):
            epk = headers.get("epk")
            if epk is None:
                msg = "No EPK in the headers!"
                raise ValueError(msg)
            epk_jwk = Jwk(epk)
            if epk_jwk.is_private:
                msg = "The EPK present in the header is private."
                raise ValueError(msg)
            epk = epk_jwk.cryptography_key
            if isinstance(key_alg_wrapper, BaseEcdhEs_AesKw):
                cek = key_alg_wrapper.unwrap_key_with_epk(wrapped_cek, epk, alg=key_alg_wrapper.name)
            else:
                cek = key_alg_wrapper.recipient_key(
                    epk,
                    alg=enc_alg_class.name,
                    key_size=enc_alg_class.key_size,
                    **headers,
                )

        elif isinstance(key_alg_wrapper, BaseAesKeyWrap):
            cek = key_alg_wrapper.unwrap_key(wrapped_cek)

        elif isinstance(key_alg_wrapper, BaseAesGcmKeyWrap):
            iv = headers.get("iv")
            if iv is None:
                msg = "No 'iv' in headers!"
                raise ValueError(msg)
            iv = BinaPy(iv).decode_from("b64u")
            tag = headers.get("tag")
            if tag is None:
                msg = "No 'tag' in headers!"
                raise ValueError(msg)
            tag = BinaPy(tag).decode_from("b64u")
            cek = key_alg_wrapper.unwrap_key(wrapped_cek, tag=tag, iv=iv)

        elif isinstance(key_alg_wrapper, DirectKeyUse):
            cek = key_alg_wrapper.direct_key(enc_alg_class)

        else:
            msg = f"Unsupported Key Management Alg {key_alg_wrapper}"
            raise UnsupportedAlg(msg)  # pragma: no cover

        return SymmetricJwk.from_bytes(cek)

    def public_jwk(self) -> Jwk:
        """Return the public Jwk associated with this key.

        Returns:
          a Jwk with the public key

        """
        if not self.is_private:
            return self

        params = {name: self.get(name) for name, param in self.PARAMS.items() if not param.is_private}

        if "key_ops" in self:
            key_ops = list(self.key_ops)
            if "sign" in key_ops:
                key_ops.remove("sign")
                key_ops.append("verify")
            if "unwrapKey" in key_ops:
                key_ops.remove("unwrapKey")
                key_ops.append("wrapKey")
        else:
            key_ops = None

        return Jwk(
            dict(
                kty=self.kty,
                kid=self.get("kid"),
                alg=self.get("alg"),
                use=self.get("use"),
                key_ops=key_ops,
                **params,
            )
        )

    def as_jwks(self) -> JwkSet:
        """Return a JwkSet with this key as single element.

        Returns:
            a JwsSet with this single key

        """
        from .jwks import JwkSet

        return JwkSet(keys=(self,))

    @classmethod
    def from_cryptography_key(cls, cryptography_key: Any, **kwargs: Any) -> Jwk:
        """Initialize a Jwk from a key from the `cryptography` library.

        The input key can be any private or public key supported by cryptography.

        Args:
          cryptography_key: a `cryptography` key instance
          **kwargs: additional members to include in the Jwk (e.g. kid, use)

        Returns:
            the matching `Jwk` instance

        Raises:
            TypeError: if the key type is not supported

        """
        for jwk_class in Jwk.__subclasses__():
            for cryptography_class in (
                jwk_class.CRYPTOGRAPHY_PRIVATE_KEY_CLASSES + jwk_class.CRYPTOGRAPHY_PUBLIC_KEY_CLASSES
            ):
                if isinstance(cryptography_key, cryptography_class):
                    return jwk_class.from_cryptography_key(cryptography_key, **kwargs)

        msg = f"Unsupported Jwk class for this Key Type: {type(cryptography_key).__name__}"
        raise TypeError(msg)

    def _to_cryptography_key(self) -> Any:
        """Return a key from the `cryptography` library that matches this Jwk.

        This is implemented by subclasses.

        Returns:
            a `cryptography`key instance initialized from the current key

        """
        raise NotImplementedError

    @classmethod
    def from_pem(
        cls,
        pem: bytes | str,
        password: bytes | str | None = None,
        **kwargs: Any,
    ) -> Jwk:
        """Load a `Jwk` from a PEM encoded private or public key.

        Args:
          pem: the PEM encoded data to load
          password: the password to decrypt the PEM, if required. Should be bytes.
              If it is a string, it will be encoded with UTF-8.
          **kwargs: additional members to include in the `Jwk` (e.g. `kid`, `use`)

        Returns:
            a `Jwk` instance from the loaded key

        """
        pem = pem.encode() if isinstance(pem, str) else pem
        password = password.encode("UTF-8") if isinstance(password, str) else password

        try:
            cryptography_key = serialization.load_pem_private_key(pem, password)
        except Exception as private_exc:
            try:
                cryptography_key = serialization.load_pem_public_key(pem)

            except Exception:
                msg = "The provided data is not a private or a public PEM encoded key."
                raise ValueError(msg) from private_exc
            if password is not None:
                msg = (
                    "A public key was loaded from PEM, while a password was provided for decryption. "
                    "Only private keys are encrypted using a password."
                )
                raise ValueError(msg) from None

        return cls.from_cryptography_key(cryptography_key, **kwargs)

    def to_pem(self, password: bytes | str | None = None) -> str:
        """Serialize this key to PEM format.

        For private keys, you can provide a password for encryption. This password should be `bytes`. A `str` is also
        accepted, and will be encoded to `bytes` using UTF-8 before it is used as encryption key.

        Args:
          password: password to use to encrypt the PEM.

        Returns:
            the PEM serialized key

        """
        password = password.encode("UTF-8") if isinstance(password, str) else password

        if self.is_private:
            encryption: serialization.KeySerializationEncryption
            encryption = serialization.BestAvailableEncryption(password) if password else serialization.NoEncryption()
            return self.cryptography_key.private_bytes(  # type: ignore[no-any-return]
                serialization.Encoding.PEM,
                serialization.PrivateFormat.PKCS8,
                encryption,
            ).decode()
        else:
            if password:
                msg = "Public keys cannot be encrypted when serialized."
                raise ValueError(msg)
            return self.cryptography_key.public_bytes(  # type: ignore[no-any-return]
                serialization.Encoding.PEM,
                serialization.PublicFormat.SubjectPublicKeyInfo,
            ).decode()

    @classmethod
    def from_der(
        cls,
        der: bytes,
        password: bytes | str | None = None,
        **kwargs: Any,
    ) -> Jwk:
        """Load a `Jwk` from DER."""
        password = password.encode("UTF-8") if isinstance(password, str) else password

        try:
            cryptography_key = serialization.load_der_private_key(der, password)
        except Exception as private_exc:
            try:
                cryptography_key = serialization.load_der_public_key(der)
            except Exception:
                msg = "The provided data is not a private or a public DER encoded key."
                raise ValueError(msg) from private_exc
            if password is not None:
                msg = (
                    "A public key was loaded from DER, while a password was provided for decryption. "
                    "Only private keys are encrypted using a password."
                )
                raise ValueError(msg) from None

        return cls.from_cryptography_key(cryptography_key, **kwargs)

    def to_der(self, password: bytes | str | None = None) -> BinaPy:
        """Serialize this key to DER.

        For private keys, you can provide a password for encryption. This password should be bytes. A `str` is also
        accepted, and will be encoded to `bytes` using UTF-8 before it is used as encryption key.

        Args:
          password: password to use to encrypt the PEM. Should be bytes.
            If it is a string, it will be encoded to bytes with UTF-8.

        Returns:
            the DER serialized key

        """
        password = password.encode("UTF-8") if isinstance(password, str) else password

        if self.is_private:
            encryption: serialization.KeySerializationEncryption
            encryption = serialization.BestAvailableEncryption(password) if password else serialization.NoEncryption()
            return BinaPy(
                self.cryptography_key.private_bytes(
                    serialization.Encoding.DER,
                    serialization.PrivateFormat.PKCS8,
                    encryption,
                )
            )
        else:
            if password:
                msg = "Public keys cannot be encrypted when serialized."
                raise ValueError(msg)
            return BinaPy(
                self.cryptography_key.public_bytes(
                    serialization.Encoding.DER,
                    serialization.PublicFormat.SubjectPublicKeyInfo,
                )
            )

    @classmethod
    def from_x509(cls, x509_pem: str | bytes) -> Self:
        """Read the public key from a X509 certificate, PEM formatted."""
        if isinstance(x509_pem, str):
            x509_pem = x509_pem.encode()

        cert = x509.load_pem_x509_certificate(x509_pem)
        return cls(cert.public_key())

    @classmethod
    def generate(cls, *, alg: str | None = None, kty: str | None = None, **kwargs: Any) -> Jwk:
        """Generate a Private Key and return it as a `Jwk` instance.

        This method is implemented by subclasses for specific Key Types and returns an instance of that subclass.

        Args:
            alg: intended algorithm to use with the generated key
            kty: key type identifier
            **kwargs: specific parameters depending on the type of key, or additional members to include in the `Jwk`

        Returns:
            a `Jwk` instance with a generated key

        """
        if alg:
            key = cls.generate_for_alg(alg=alg, **kwargs)
            if kty is not None and key.kty != kty:
                msg = f"Incompatible `{alg=}` and `{kty=}` parameters. `{alg=}` points to `kty='{key.kty}'`."
                raise ValueError(msg)
            return key
        if kty:
            return cls.generate_for_kty(kty=kty, **kwargs)
        msg = (
            "You must provide a hint for jwskate to know what kind of key it must generate. "
            "You can either provide an 'alg' identifier as keyword parameter, and/or a 'kty'."
        )
        raise ValueError(msg)

    def copy(self) -> Jwk:
        """Create a copy of this key.

        Returns:
            a copy of this key, with the same value

        """
        return Jwk(copy(self.data))

    def with_kid_thumbprint(self, *, force: bool = False) -> Jwk:
        """Include the JWK thumbprint as `kid`.

        If key already has a `kid` (Key ID):

        - if `force` is `True`, this erases the previous "kid".
        - if `force` is `False` (default), do nothing.

        Args:
            force: whether to overwrite a previously existing kid

        Returns:
            a copy of this Jwk, with a `kid` attribute.

        """
        jwk = self.copy()
        if self.get("kid") is not None and not force:
            return jwk
        jwk["kid"] = self.thumbprint()
        return jwk

    def with_usage_parameters(
        self,
        alg: str | None = None,
        *,
        with_alg: bool = True,
        with_use: bool = True,
        with_key_ops: bool = True,
    ) -> Jwk:
        """Copy this Jwk and add the `use` and `key_ops` parameters.

        The returned jwk `alg` parameter will be the one passed as parameter to this method,
        or as default the one declared as `alg` parameter in this Jwk.

        The `use` (Public Key Use) param is deduced based on this `alg` value.

        The `key_ops` (Key Operations) param is deduced based on the key `use` and if the key is public, private,
        or symmetric.

        Args:
            alg: the alg to use, if not present in this Jwk
            with_alg: whether to include an `alg` parameter
            with_use: whether to include a `use` parameter
            with_key_ops: whether to include a `key_ops` parameter

        Returns:
            a Jwk with the same key, with `alg`, `use` and `key_ops` parameters.

        """
        alg = alg or self.alg

        if not alg:
            msg = "An algorithm is required to set the usage parameters"
            raise ExpectedAlgRequired(msg)

        self._get_alg_class(alg)  # raises an exception if alg is not supported

        jwk = self.copy()
        if with_alg:
            jwk["alg"] = alg
        if with_use:
            jwk["use"] = jwk.use
        if with_key_ops:
            jwk["key_ops"] = jwk.key_ops

        return jwk

    def minimize(self) -> Jwk:
        """Strip out any optional or non-standard parameter from that key.

        This will remove `alg`, `use`, `key_ops`, optional parameters from RSA keys, and other
        unknown parameters.

        """
        jwk = self.copy()
        for key in self.keys():
            if key == "kty" or key in self.PARAMS and self.PARAMS[key].is_required:
                continue
            del jwk[key]

        return jwk

    def __eq__(self, other: Any) -> bool:
        """Compare JWK keys, ignoring optional/informational fields."""
        other = to_jwk(other)
        return super(Jwk, self.minimize()).__eq__(other.minimize())

is_private property

1
is_private: bool

Return True if the key is private, False otherwise.

Returns:

Type Description
bool

True if the key is private, False otherwise

is_symmetric property

1
is_symmetric: bool

Return True if the key is symmetric, False otherwise.

kty property

1
kty: str

Return the Key Type.

Returns:

Type Description
str

the key type

alg property

1
alg: str | None

Return the configured key alg, if any.

Returns:

Type Description
str | None

the key alg

kid property

1
kid: str

Return the JWK key ID (kid).

If the kid is not explicitly set, the RFC7638 key thumbprint is returned.

use property

1
use: str | None

Return the key use.

If no alg parameter is present, this returns the use parameter from this JWK. If an alg parameter is present, the use is deduced from this alg. To check for the presence of the use parameter, use jwk.get('use').

key_ops property

1
key_ops: tuple[str, ...]

Return the key operations.

If no alg parameter is present, this returns the key_ops parameter from this JWK. If an alg parameter is present, the key operations are deduced from this alg. To check for the presence of the key_ops parameter, use jwk.get('key_ops').

generate_for_alg classmethod

1
generate_for_alg(alg: str, **kwargs: Any) -> Jwk

Generate a key for usage with a specific alg and return the resulting Jwk.

Parameters:

Name Type Description Default
alg str

a signature or key management algorithm identifier

required
**kwargs Any

specific parameters, depending on the key type, or additional members to include in the Jwk

{}

Returns:

Type Description
Jwk

the generated Jwk

Source code in jwskate/jwk/base.py
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
@classmethod
def generate_for_alg(cls, alg: str, **kwargs: Any) -> Jwk:
    """Generate a key for usage with a specific `alg` and return the resulting `Jwk`.

    Args:
        alg: a signature or key management algorithm identifier
        **kwargs: specific parameters, depending on the key type, or additional members to include in the `Jwk`

    Returns:
        the generated `Jwk`

    """
    for jwk_class in Jwk.__subclasses__():
        try:
            jwk_class._get_alg_class(alg)
            return jwk_class.generate(alg=alg, **kwargs)
        except UnsupportedAlg:
            continue

    raise UnsupportedAlg(alg)

generate_for_kty classmethod

1
generate_for_kty(kty: str, **kwargs: Any) -> Jwk

Generate a key with a specific type and return the resulting Jwk.

Parameters:

Name Type Description Default
kty str

key type to generate

required
**kwargs Any

specific parameters depending on the key type, or additional members to include in the Jwk

{}

Returns:

Type Description
Jwk

the resulting Jwk

Raises:

Type Description
UnsupportedKeyType

if the specified key type (kty) is not supported

Source code in jwskate/jwk/base.py
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
@classmethod
def generate_for_kty(cls, kty: str, **kwargs: Any) -> Jwk:
    """Generate a key with a specific type and return the resulting `Jwk`.

    Args:
      kty: key type to generate
      **kwargs: specific parameters depending on the key type, or additional members to include in the `Jwk`

    Returns:
        the resulting `Jwk`

    Raises:
        UnsupportedKeyType: if the specified key type (`kty`) is not supported

    """
    for jwk_class in Jwk.__subclasses__():
        if kty == jwk_class.KTY:
            return jwk_class.generate(**kwargs)
    msg = "Unsupported Key Type:"
    raise UnsupportedKeyType(msg, kty)

__new__

1
2
3
__new__(
    key: Jwk | Mapping[str, Any] | Any, **kwargs: Any
) -> Jwk

Overridden __new__ to make the Jwk constructor smarter.

The Jwk constructor will accept:

1
2
3
- a `dict` with the parsed Jwk content
- another `Jwk`, which will be used as-is instead of creating a copy
- an instance from a `cryptography` public or private key class

Parameters:

Name Type Description Default
key Jwk | Mapping[str, Any] | Any

the source for key materials

required
**kwargs Any

additional members to include in the Jwk

{}
Source code in jwskate/jwk/base.py
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
def __new__(cls, key: Jwk | Mapping[str, Any] | Any, **kwargs: Any) -> Jwk:
    """Overridden `__new__` to make the Jwk constructor smarter.

    The `Jwk` constructor will accept:

        - a `dict` with the parsed Jwk content
        - another `Jwk`, which will be used as-is instead of creating a copy
        - an instance from a `cryptography` public or private key class

    Args:
        key: the source for key materials
        **kwargs: additional members to include in the Jwk

    """
    if cls == Jwk:
        if isinstance(key, Jwk):
            return cls.from_cryptography_key(key.cryptography_key, **kwargs)
        if isinstance(key, Mapping):
            kty: str | None = key.get("kty")
            if kty is None:
                msg = "A Json Web Key must have a Key Type (kty)"
                raise InvalidJwk(msg)

            for jwk_class in Jwk.__subclasses__():
                if kty == jwk_class.KTY:
                    return super().__new__(jwk_class)

            msg = "Unsupported Key Type"
            raise InvalidJwk(msg, kty)

        elif isinstance(key, str):
            return cls.from_json(key)
        else:
            return cls.from_cryptography_key(key, **kwargs)
    return super().__new__(cls)

_get_alg_class classmethod

1
_get_alg_class(alg: str) -> type[BaseAlg]

Given an alg identifier, return the matching JWA wrapper.

Parameters:

Name Type Description Default
alg str

an alg identifier

required

Returns:

Type Description
type[BaseAlg]

the matching JWA wrapper

Source code in jwskate/jwk/base.py
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
@classmethod
def _get_alg_class(cls, alg: str) -> type[BaseAlg]:
    """Given an alg identifier, return the matching JWA wrapper.

    Args:
        alg: an alg identifier

    Returns:
        the matching JWA wrapper

    """
    alg_class: type[BaseAlg] | None

    alg_class = cls.SIGNATURE_ALGORITHMS.get(alg)
    if alg_class is not None:
        return alg_class

    alg_class = cls.KEY_MANAGEMENT_ALGORITHMS.get(alg)
    if alg_class is not None:
        return alg_class

    alg_class = cls.ENCRYPTION_ALGORITHMS.get(alg)
    if alg_class is not None:
        return alg_class

    raise UnsupportedAlg(alg)

__getattr__

1
__getattr__(param: str) -> Any

Allow access to key parameters as attributes.

This is a convenience to allow jwk.param instead of jwk['param'].

Parameters:

Name Type Description Default
param str

the parameter name to access

required
Return

the param value

Raises:

Type Description
AttributeError

if the param is not found

Source code in jwskate/jwk/base.py
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
def __getattr__(self, param: str) -> Any:
    """Allow access to key parameters as attributes.

    This is a convenience to allow `jwk.param` instead of `jwk['param']`.

    Args:
        param: the parameter name to access

    Return:
        the param value

    Raises:
        AttributeError: if the param is not found

    """
    value = self.get(param)
    if value is None:
        raise AttributeError(param)
    return value

__setitem__

1
__setitem__(key: str, value: Any) -> None

Override base method to avoid modifying cryptographic key attributes.

Parameters:

Name Type Description Default
key str

name of the attribute to set

required
value Any

value to set

required

Raises:

Type Description
RuntimeError

when trying to modify cryptographic attributes

Source code in jwskate/jwk/base.py
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
def __setitem__(self, key: str, value: Any) -> None:
    """Override base method to avoid modifying cryptographic key attributes.

    Args:
        key: name of the attribute to set
        value: value to set

    Raises:
        RuntimeError: when trying to modify cryptographic attributes

    """
    # don't allow modifying private attributes after the key has been initialized
    if key in self.PARAMS and hasattr(self, "cryptography_key"):
        msg = "JWK key attributes cannot be modified."
        raise RuntimeError(msg)
    super().__setitem__(key, value)

thumbprint

1
thumbprint(hashalg: str = 'sha-256') -> str

Return the key thumbprint as specified by RFC 7638.

Parameters:

Name Type Description Default
hashalg str

A hash function (defaults to SHA256)

'sha-256'

Returns:

Type Description
str

the calculated thumbprint

Source code in jwskate/jwk/base.py
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
def thumbprint(self, hashalg: str = "sha-256") -> str:
    """Return the key thumbprint as specified by RFC 7638.

    Args:
      hashalg: A hash function (defaults to SHA256)

    Returns:
        the calculated thumbprint

    """
    alg = self.IANA_HASH_FUNCTION_NAMES.get(hashalg)
    if not alg:
        msg = f"Unsupported hash alg {hashalg}"
        raise ValueError(msg)

    t = {"kty": self.get("kty")}
    for name, param in self.PARAMS.items():
        if param.is_required and not param.is_private:
            t[name] = self.get(name)

    return BinaPy.serialize_to("json", t, separators=(",", ":"), sort_keys=True).to(alg).to("b64u").ascii()

thumbprint_uri

1
thumbprint_uri(hashalg: str = 'sha-256') -> str

Return the JWK thumbprint URI for this key.

Parameters:

Name Type Description Default
hashalg str

the IANA registered name for the hash alg to use

'sha-256'

Returns:

Type Description
str

the JWK thumbprint URI for this Jwk

Source code in jwskate/jwk/base.py
396
397
398
399
400
401
402
403
404
405
406
407
def thumbprint_uri(self, hashalg: str = "sha-256") -> str:
    """Return the JWK thumbprint URI for this key.

    Args:
        hashalg: the IANA registered name for the hash alg to use

    Returns:
         the JWK thumbprint URI for this `Jwk`

    """
    thumbprint = self.thumbprint(hashalg)
    return f"urn:ietf:params:oauth:jwk-thumbprint:{hashalg}:{thumbprint}"

check

1
2
3
4
5
6
check(
    *,
    is_private: bool | None = None,
    is_symmetric: bool | None = None,
    kty: str | None = None
) -> Jwk

Check this key for type, privateness and/or symmetricness.

This raises a ValueError if the key is not as expected.

Parameters:

Name Type Description Default
is_private bool | None
  • if True, check if the key is private,
  • if False, check if it is public,
  • if None, do nothing
None
is_symmetric bool | None
  • if True, check if the key is symmetric,
  • if False, check if it is asymmetric,
  • if None, do nothing
None
kty str | None

the expected key type, if any

None

Returns:

Type Description
Jwk

this key, if all checks passed

Raises:

Type Description
ValueError

if any check fails

Source code in jwskate/jwk/base.py
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
def check(
    self,
    *,
    is_private: bool | None = None,
    is_symmetric: bool | None = None,
    kty: str | None = None,
) -> Jwk:
    """Check this key for type, privateness and/or symmetricness.

    This raises a `ValueError` if the key is not as expected.

    Args:
        is_private:

            - if `True`, check if the key is private,
            - if `False`, check if it is public,
            - if `None`, do nothing
        is_symmetric:

            - if `True`, check if the key is symmetric,
            - if `False`, check if it is asymmetric,
            - if `None`, do nothing
        kty: the expected key type, if any

    Returns:
        this key, if all checks passed

    Raises:
        ValueError: if any check fails

    """
    if is_private is not None:
        if is_private and not self.is_private:
            msg = "This key is public while a private key is expected."
            raise ValueError(msg)
        elif not is_private and self.is_private:
            msg = "This key is private while a public key is expected."
            raise ValueError(msg)

    if is_symmetric is not None:
        if is_symmetric and not self.is_symmetric:
            msg = "This key is asymmetric while a symmetric key is expected."
            raise ValueError(msg)
        if not is_symmetric and self.is_symmetric:
            msg = "This key is symmetric while an asymmetric key is expected."
            raise ValueError(msg)

    if kty is not None and self.kty != kty:
        msg = f"This key has kty={self.kty} while a kty={kty} is expected."
        raise ValueError(msg)

    return self

_validate

1
_validate() -> None

Validate the content of this Jwk.

It checks that all required parameters are present and well-formed. If the key is private, it sets the is_private flag to True.

Raises:

Type Description
TypeError

if the key type doesn't match the subclass

InvalidJwk

if the JWK misses required members or has invalid members

Source code in jwskate/jwk/base.py
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
def _validate(self) -> None:  # noqa: C901
    """Validate the content of this `Jwk`.

    It checks that all required parameters are present and well-formed.
    If the key is private, it sets the `is_private` flag to `True`.

    Raises:
        TypeError: if the key type doesn't match the subclass
        InvalidJwk: if the JWK misses required members or has invalid members

    """
    if self.get("kty") != self.KTY:
        msg = f"This key 'kty' {self.get('kty')} doesn't match this Jwk subclass intended 'kty' {self.KTY}!"
        raise TypeError(msg)

    jwk_is_private = False
    for name, param in self.PARAMS.items():
        value = self.get(name)

        if param.is_private and value is not None:
            jwk_is_private = True

        if not param.is_private and param.is_required and value is None:
            msg = f"Missing required public param {param.description} ({name})"
            raise InvalidJwk(msg)

        if value is None:
            pass
        elif param.kind == "b64u":
            if not isinstance(value, str):
                msg = f"Parameter {param.description} ({name}) must be a string with a Base64URL-encoded value"
                raise InvalidJwk(msg)
            if not BinaPy(value).check("b64u"):
                msg = f"Parameter {param.description} ({name}) must be a Base64URL-encoded value"
                raise InvalidJwk(msg)
        elif param.kind == "unsupported":
            if value is not None:  # pragma: no cover
                msg = f"Unsupported JWK param '{name}'"
                raise InvalidJwk(msg)
        elif param.kind == "name":
            pass
        else:
            msg = f"Unsupported param '{name}' type '{param.kind}'"
            raise AssertionError(msg)  # pragma: no cover

    # if at least one of the supplied parameter was private, then all required private parameters must be provided
    if jwk_is_private:
        for name, param in self.PARAMS.items():
            value = self.get(name)
            if param.is_private and param.is_required and value is None:
                msg = f"Missing required private param {param.description} ({name})"
                raise InvalidJwk(msg)

    # if key is used for signing, it must be private
    for op in self.get("key_ops", []):
        if op in ("sign", "unwrapKey") and not self.is_private:
            msg = f"Key Operation is '{op}' but the key is public"
            raise InvalidJwk(msg)

signature_class

1
2
3
signature_class(
    alg: str | None = None,
) -> type[BaseSignatureAlg]

Return the appropriate signature algorithm class to use with this key.

The returned class is a BaseSignatureAlg subclass.

If this key doesn't have an alg parameter, you must supply one as parameter to this method.

Parameters:

Name Type Description Default
alg str | None

the algorithm identifier, if not already present in this Jwk

None

Returns:

Type Description
type[BaseSignatureAlg]

the appropriate BaseSignatureAlg subclass

Source code in jwskate/jwk/base.py
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
def signature_class(self, alg: str | None = None) -> type[BaseSignatureAlg]:
    """Return the appropriate signature algorithm class to use with this key.

    The returned class is a `BaseSignatureAlg` subclass.

    If this key doesn't have an `alg` parameter, you must supply one as parameter to this method.

    Args:
        alg: the algorithm identifier, if not already present in this Jwk

    Returns:
        the appropriate `BaseSignatureAlg` subclass

    """
    return select_alg_class(self.SIGNATURE_ALGORITHMS, jwk_alg=self.alg, alg=alg)

encryption_class

1
2
3
encryption_class(
    alg: str | None = None,
) -> type[BaseAESEncryptionAlg]

Return the appropriate encryption algorithm class to use with this key.

The returned class is a subclass of BaseAESEncryptionAlg.

If this key doesn't have an alg parameter, you must supply one as parameter to this method.

Parameters:

Name Type Description Default
alg str | None

the algorithm identifier, if not already present in this Jwk

None

Returns:

Type Description
type[BaseAESEncryptionAlg]

the appropriate BaseAESEncryptionAlg subclass

Source code in jwskate/jwk/base.py
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
def encryption_class(self, alg: str | None = None) -> type[BaseAESEncryptionAlg]:
    """Return the appropriate encryption algorithm class to use with this key.

    The returned class is a subclass of `BaseAESEncryptionAlg`.

    If this key doesn't have an `alg` parameter, you must supply one as parameter to this method.

    Args:
        alg: the algorithm identifier, if not already present in this Jwk

    Returns:
        the appropriate `BaseAESEncryptionAlg` subclass

    """
    return select_alg_class(self.ENCRYPTION_ALGORITHMS, jwk_alg=self.alg, alg=alg)

key_management_class

1
2
3
key_management_class(
    alg: str | None = None,
) -> type[BaseKeyManagementAlg]

Return the appropriate key management algorithm class to use with this key.

If this key doesn't have an alg parameter, you must supply one as parameter to this method.

Parameters:

Name Type Description Default
alg str | None

the algorithm identifier, if not already present in this Jwk

None

Returns:

Type Description
type[BaseKeyManagementAlg]

the appropriate BaseKeyManagementAlg subclass

Source code in jwskate/jwk/base.py
553
554
555
556
557
558
559
560
561
562
563
564
565
def key_management_class(self, alg: str | None = None) -> type[BaseKeyManagementAlg]:
    """Return the appropriate key management algorithm class to use with this key.

    If this key doesn't have an `alg` parameter, you must supply one as parameter to this method.

    Args:
        alg: the algorithm identifier, if not already present in this Jwk

    Returns:
        the appropriate `BaseKeyManagementAlg` subclass

    """
    return select_alg_class(self.KEY_MANAGEMENT_ALGORITHMS, jwk_alg=self.alg, alg=alg)

signature_wrapper

1
2
3
signature_wrapper(
    alg: str | None = None,
) -> BaseSignatureAlg

Initialize a key management wrapper with this key.

This returns an instance of a BaseSignatureAlg subclass.

If this key doesn't have an alg parameter, you must supply one as parameter to this method.

Parameters:

Name Type Description Default
alg str | None

the algorithm identifier, if not already present in this Jwk

None

Returns:

Type Description
BaseSignatureAlg

a BaseSignatureAlg instance initialized with the current key

Source code in jwskate/jwk/base.py
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
def signature_wrapper(self, alg: str | None = None) -> BaseSignatureAlg:
    """Initialize a  key management wrapper with this key.

    This returns an instance of a `BaseSignatureAlg` subclass.

    If this key doesn't have an `alg` parameter, you must supply one as parameter to this method.

    Args:
        alg: the algorithm identifier, if not already present in this Jwk

    Returns:
        a `BaseSignatureAlg` instance initialized with the current key

    """
    alg_class = self.signature_class(alg)
    if issubclass(alg_class, BaseSymmetricAlg):
        return alg_class(self.key)
    elif issubclass(alg_class, BaseAsymmetricAlg):
        return alg_class(self.cryptography_key)
    raise UnsupportedAlg(alg)  # pragma: no cover

encryption_wrapper

1
2
3
encryption_wrapper(
    alg: str | None = None,
) -> BaseAESEncryptionAlg

Initialize an encryption wrapper with this key.

This returns an instance of a BaseAESEncryptionAlg subclass.

If this key doesn't have an alg parameter, you must supply one as parameter to this method.

Parameters:

Name Type Description Default
alg str | None

the algorithm identifier, if not already present in this Jwk

None

Returns:

Type Description
BaseAESEncryptionAlg

a BaseAESEncryptionAlg instance initialized with the current key

Source code in jwskate/jwk/base.py
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
def encryption_wrapper(self, alg: str | None = None) -> BaseAESEncryptionAlg:
    """Initialize an encryption wrapper with this key.

    This returns an instance of a `BaseAESEncryptionAlg` subclass.

    If this key doesn't have an `alg` parameter, you must supply one as parameter to this method.

    Args:
        alg: the algorithm identifier, if not already present in this Jwk

    Returns:
        a `BaseAESEncryptionAlg` instance initialized with the current key

    """
    alg_class = self.encryption_class(alg)
    if issubclass(alg_class, BaseSymmetricAlg):
        return alg_class(self.key)
    elif issubclass(alg_class, BaseAsymmetricAlg):  # pragma: no cover
        return alg_class(self.cryptography_key)  # pragma: no cover
    raise UnsupportedAlg(alg)  # pragma: no cover

key_management_wrapper

1
2
3
key_management_wrapper(
    alg: str | None = None,
) -> BaseKeyManagementAlg

Initialize a key management wrapper with this key.

This returns an instance of a BaseKeyManagementAlg subclass.

If this key doesn't have an alg parameter, you must supply one as parameter to this method.

Parameters:

Name Type Description Default
alg str | None

the algorithm identifier, if not already present in this Jwk

None

Returns:

Type Description
BaseKeyManagementAlg

a BaseKeyManagementAlg instance initialized with the current key

Source code in jwskate/jwk/base.py
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
def key_management_wrapper(self, alg: str | None = None) -> BaseKeyManagementAlg:
    """Initialize a key management wrapper with this key.

    This returns an instance of a `BaseKeyManagementAlg` subclass.

    If this key doesn't have an `alg` parameter, you must supply one as parameter to this method.

    Args:
        alg: the algorithm identifier, if not already present in this Jwk

    Returns:
        a `BaseKeyManagementAlg` instance initialized with the current key

    """
    alg_class = self.key_management_class(alg)
    if issubclass(alg_class, BaseSymmetricAlg):
        return alg_class(self.key)
    elif issubclass(alg_class, BaseAsymmetricAlg):
        return alg_class(self.cryptography_key)
    raise UnsupportedAlg(alg)  # pragma: no cover

supported_signing_algorithms

1
supported_signing_algorithms() -> list[str]

Return the list of Signature algorithms that can be used with this key.

Returns:

Type Description
list[str]

a list of supported algs

Source code in jwskate/jwk/base.py
630
631
632
633
634
635
636
637
def supported_signing_algorithms(self) -> list[str]:
    """Return the list of Signature algorithms that can be used with this key.

    Returns:
      a list of supported algs

    """
    return list(self.SIGNATURE_ALGORITHMS)

supported_key_management_algorithms

1
supported_key_management_algorithms() -> list[str]

Return the list of Key Management algorithms that can be used with this key.

Returns:

Type Description
list[str]

a list of supported algs

Source code in jwskate/jwk/base.py
639
640
641
642
643
644
645
646
def supported_key_management_algorithms(self) -> list[str]:
    """Return the list of Key Management algorithms that can be used with this key.

    Returns:
        a list of supported algs

    """
    return list(self.KEY_MANAGEMENT_ALGORITHMS)

supported_encryption_algorithms

1
supported_encryption_algorithms() -> list[str]

Return the list of Encryption algorithms that can be used with this key.

Returns:

Type Description
list[str]

a list of supported algs

Source code in jwskate/jwk/base.py
648
649
650
651
652
653
654
655
def supported_encryption_algorithms(self) -> list[str]:
    """Return the list of Encryption algorithms that can be used with this key.

    Returns:
        a list of supported algs

    """
    return list(self.ENCRYPTION_ALGORITHMS)

sign

1
2
3
sign(
    data: bytes | SupportsBytes, alg: str | None = None
) -> BinaPy

Sign data using this Jwk, and return the generated signature.

Parameters:

Name Type Description Default
data bytes | SupportsBytes

the data to sign

required
alg str | None

the alg to use (if this key doesn't have an alg parameter)

None

Returns:

Type Description
BinaPy

the generated signature

Source code in jwskate/jwk/base.py
657
658
659
660
661
662
663
664
665
666
667
668
669
670
def sign(self, data: bytes | SupportsBytes, alg: str | None = None) -> BinaPy:
    """Sign data using this Jwk, and return the generated signature.

    Args:
      data: the data to sign
      alg: the alg to use (if this key doesn't have an `alg` parameter)

    Returns:
      the generated signature

    """
    wrapper = self.signature_wrapper(alg)
    signature = wrapper.sign(data)
    return BinaPy(signature)

verify

1
2
3
4
5
6
7
verify(
    data: bytes | SupportsBytes,
    signature: bytes | SupportsBytes,
    *,
    alg: str | None = None,
    algs: Iterable[str] | None = None
) -> bool

Verify a signature using this Jwk, and return True if valid.

Parameters:

Name Type Description Default
data bytes | SupportsBytes

the data to verify

required
signature bytes | SupportsBytes

the signature to verify

required
alg str | None

the allowed signature alg, if there is only one

None
algs Iterable[str] | None

the allowed signature algs, if there are several

None

Returns:

Type Description
bool

True if the signature matches, False otherwise

Source code in jwskate/jwk/base.py
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
def verify(
    self,
    data: bytes | SupportsBytes,
    signature: bytes | SupportsBytes,
    *,
    alg: str | None = None,
    algs: Iterable[str] | None = None,
) -> bool:
    """Verify a signature using this `Jwk`, and return `True` if valid.

    Args:
      data: the data to verify
      signature: the signature to verify
      alg: the allowed signature alg, if there is only one
      algs: the allowed signature algs, if there are several

    Returns:
      `True` if the signature matches, `False` otherwise

    """
    if not self.is_symmetric and self.is_private:
        warnings.warn(
            "You are trying to validate a signature with a private key. "
            "Signatures should always be verified with a public key.",
            stacklevel=2,
        )
        public_jwk = self.public_jwk()
    else:
        public_jwk = self
    if algs is None and alg:
        algs = [alg]
    for alg in algs or (None,):
        wrapper = public_jwk.signature_wrapper(alg)
        if wrapper.verify(data, signature):
            return True

    return False

encrypt

1
2
3
4
5
6
7
encrypt(
    plaintext: bytes | SupportsBytes,
    *,
    aad: bytes | None = None,
    alg: str | None = None,
    iv: bytes | None = None
) -> tuple[BinaPy, BinaPy, BinaPy]

Encrypt a plaintext with Authenticated Encryption using this key.

Authenticated Encryption with Associated Data (AEAD) is supported, by passing Additional Authenticated Data (aad).

This returns a tuple with 3 raw data, in order: - the encrypted Data - the Initialization Vector that was used to encrypt data - the generated Authentication Tag

Parameters:

Name Type Description Default
plaintext bytes | SupportsBytes

the data to encrypt.

required
aad bytes | None

the Additional Authenticated Data (AAD) to include in the authentication tag

None
alg str | None

the alg to use to encrypt the data

None
iv bytes | None

the Initialization Vector to use. If None, an IV is randomly generated. If a value is provided, the returned IV will be that same value. You should never reuse the same IV!

None

Returns:

Type Description
tuple[BinaPy, BinaPy, BinaPy]

a tuple (ciphertext, iv, authentication_tag), as raw data

Source code in jwskate/jwk/base.py
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
def encrypt(
    self,
    plaintext: bytes | SupportsBytes,
    *,
    aad: bytes | None = None,
    alg: str | None = None,
    iv: bytes | None = None,
) -> tuple[BinaPy, BinaPy, BinaPy]:
    """Encrypt a plaintext with Authenticated Encryption using this key.

    Authenticated Encryption with Associated Data (AEAD) is supported,
    by passing Additional Authenticated Data (`aad`).

    This returns a tuple with 3 raw data, in order:
    - the encrypted Data
    - the Initialization Vector that was used to encrypt data
    - the generated Authentication Tag

    Args:
      plaintext: the data to encrypt.
      aad: the Additional Authenticated Data (AAD) to include in the authentication tag
      alg: the alg to use to encrypt the data
      iv: the Initialization Vector to use. If `None`, an IV is randomly generated.
          If a value is provided, the returned IV will be that same value. You should never reuse the same IV!

    Returns:
      a tuple (ciphertext, iv, authentication_tag), as raw data

    """
    raise NotImplementedError  # pragma: no cover

decrypt

1
2
3
4
5
6
7
8
decrypt(
    ciphertext: bytes | SupportsBytes,
    *,
    iv: bytes | SupportsBytes,
    tag: bytes | SupportsBytes,
    aad: bytes | SupportsBytes | None = None,
    alg: str | None = None
) -> BinaPy

Decrypt an encrypted data using this Jwk, and return the encrypted result.

This is implemented by subclasses.

Parameters:

Name Type Description Default
ciphertext bytes | SupportsBytes

the data to decrypt

required
iv bytes | SupportsBytes

the Initialization Vector (IV) that was used for encryption

required
tag bytes | SupportsBytes

the Authentication Tag that will be verified while decrypting data

required
aad bytes | SupportsBytes | None

the Additional Authentication Data (AAD) to verify the Tag against

None
alg str | None

the alg to use for decryption

None

Returns:

Type Description
BinaPy

the clear-text data

Source code in jwskate/jwk/base.py
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
def decrypt(
    self,
    ciphertext: bytes | SupportsBytes,
    *,
    iv: bytes | SupportsBytes,
    tag: bytes | SupportsBytes,
    aad: bytes | SupportsBytes | None = None,
    alg: str | None = None,
) -> BinaPy:
    """Decrypt an encrypted data using this Jwk, and return the encrypted result.

    This is implemented by subclasses.

    Args:
      ciphertext: the data to decrypt
      iv: the Initialization Vector (IV) that was used for encryption
      tag: the Authentication Tag that will be verified while decrypting data
      aad: the Additional Authentication Data (AAD) to verify the Tag against
      alg: the alg to use for decryption

    Returns:
      the clear-text data

    """
    raise NotImplementedError  # pragma: no cover

sender_key

1
2
3
4
5
6
7
8
sender_key(
    enc: str,
    *,
    alg: str | None = None,
    cek: bytes | None = None,
    epk: Jwk | None = None,
    **headers: Any
) -> tuple[Jwk, BinaPy, Mapping[str, Any]]

Produce a Content Encryption Key, to use for encryption.

This method is meant to be used by encrypted token senders. Recipients should use the matching method Jwk.recipient_key().

Returns a tuple with 3 items:

  • the clear text CEK, as a SymmetricJwk instance. Use this key to encrypt your message, but do not communicate this key to anyone!
  • the encrypted CEK, as bytes. You must send this to your recipient. This may be None for Key Management algs which derive a CEK instead of generating one.
  • extra headers depending on the Key Management algorithm, as a dict of name to values. You must send those to your recipient as well.

For algorithms that rely on a randomly generated CEK, such as RSAES or AES, you can provide that CEK instead of letting jwskate generate a safe, unique random value for you. Likewise, for algorithms that rely on an ephemeral key, you can provide an EPK that you generated yourself, instead of letting jwskate generate an appropriate value for you. Only do this if you know what you are doing!

Parameters:

Name Type Description Default
enc str

the encryption algorithm to use with the CEK

required
alg str | None

the Key Management algorithm to use to produce the CEK

None
cek bytes | None

CEK to use (leave None to have an adequate random value generated automatically)

None
epk Jwk | None

EPK to use (leave None to have an adequate ephemeral key generated automatically)

None
**headers Any

additional headers to include for the CEK derivation

{}

Returns:

Type Description
tuple[Jwk, BinaPy, Mapping[str, Any]]

a tuple (cek, wrapped_cek, additional_headers_map)

Raises:

Type Description
UnsupportedAlg

if the requested alg identifier is not supported

Source code in jwskate/jwk/base.py
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
def sender_key(  # noqa: C901
    self,
    enc: str,
    *,
    alg: str | None = None,
    cek: bytes | None = None,
    epk: Jwk | None = None,
    **headers: Any,
) -> tuple[Jwk, BinaPy, Mapping[str, Any]]:
    """Produce a Content Encryption Key, to use for encryption.

    This method is meant to be used by encrypted token senders.
    Recipients should use the matching method `Jwk.recipient_key()`.

    Returns a tuple with 3 items:

    - the clear text CEK, as a SymmetricJwk instance.
    Use this key to encrypt your message, but do not communicate this key to anyone!
    - the encrypted CEK, as bytes. You must send this to your recipient.
    This may be `None` for Key Management algs which derive a CEK instead of generating one.
    - extra headers depending on the Key Management algorithm, as a dict of name to values.
    You must send those to your recipient as well.

    For algorithms that rely on a randomly generated CEK, such as RSAES or AES, you can provide that CEK instead
    of letting `jwskate` generate a safe, unique random value for you.
    Likewise, for algorithms that rely on an ephemeral key, you can provide an EPK that you generated yourself,
    instead of letting `jwskate` generate an appropriate value for you.
    Only do this if you know what you are doing!

    Args:
      enc: the encryption algorithm to use with the CEK
      alg: the Key Management algorithm to use to produce the CEK
      cek: CEK to use (leave `None` to have an adequate random value generated automatically)
      epk: EPK to use (leave `None` to have an adequate ephemeral key generated automatically)
      **headers: additional headers to include for the CEK derivation

    Returns:
      a tuple (cek, wrapped_cek, additional_headers_map)

    Raises:
        UnsupportedAlg: if the requested alg identifier is not supported

    """
    from jwskate import SymmetricJwk

    if not self.is_symmetric and self.is_private:
        warnings.warn(
            "You are using a private key for sender key wrapping. "
            "Key wrapping should always be done using the recipient public key.",
            stacklevel=2,
        )
        key_alg_wrapper = self.public_jwk().key_management_wrapper(alg)
    else:
        key_alg_wrapper = self.key_management_wrapper(alg)

    enc_alg_class = select_alg_class(SymmetricJwk.ENCRYPTION_ALGORITHMS, alg=enc)

    cek_headers: dict[str, Any] = {}

    if isinstance(key_alg_wrapper, BaseRsaKeyWrap):
        if cek:
            enc_alg_class.check_key(cek)
        else:
            cek = enc_alg_class.generate_key()
        wrapped_cek = key_alg_wrapper.wrap_key(cek)

    elif isinstance(key_alg_wrapper, EcdhEs):
        epk = epk or Jwk.from_cryptography_key(key_alg_wrapper.generate_ephemeral_key())
        cek_headers = {"epk": epk.public_jwk()}
        if isinstance(key_alg_wrapper, BaseEcdhEs_AesKw):
            if cek:
                enc_alg_class.check_key(cek)
            else:
                cek = enc_alg_class.generate_key()
            wrapped_cek = key_alg_wrapper.wrap_key_with_epk(
                cek, epk.cryptography_key, alg=key_alg_wrapper.name, **headers
            )
        else:
            cek = key_alg_wrapper.sender_key(
                epk.cryptography_key,
                alg=enc_alg_class.name,
                key_size=enc_alg_class.key_size,
                **headers,
            )
            wrapped_cek = BinaPy(b"")

    elif isinstance(key_alg_wrapper, BaseAesKeyWrap):
        if cek:
            enc_alg_class.check_key(cek)
        else:
            cek = enc_alg_class.generate_key()
        wrapped_cek = key_alg_wrapper.wrap_key(cek)

    elif isinstance(key_alg_wrapper, BaseAesGcmKeyWrap):
        if cek:
            enc_alg_class.check_key(cek)
        else:
            cek = enc_alg_class.generate_key()
        iv = key_alg_wrapper.generate_iv()
        wrapped_cek, tag = key_alg_wrapper.wrap_key(cek, iv=iv)
        cek_headers = {
            "iv": iv.to("b64u").ascii(),
            "tag": tag.to("b64u").ascii(),
        }

    elif isinstance(key_alg_wrapper, DirectKeyUse):
        cek = key_alg_wrapper.direct_key(enc_alg_class)
        wrapped_cek = BinaPy(b"")
    else:
        msg = f"Unsupported Key Management Alg {key_alg_wrapper}"
        raise UnsupportedAlg(msg)  # pragma: no cover

    return SymmetricJwk.from_bytes(cek), wrapped_cek, cek_headers

recipient_key

1
2
3
4
5
6
7
recipient_key(
    wrapped_cek: bytes | SupportsBytes,
    enc: str,
    *,
    alg: str | None = None,
    **headers: Any
) -> Jwk

Produce a Content Encryption Key, to use for decryption.

This method is meant to be used by encrypted token recipient. Senders should use the matching method Jwk.sender_key().

Parameters:

Name Type Description Default
wrapped_cek bytes | SupportsBytes

the wrapped CEK

required
enc str

the encryption algorithm to use with the CEK

required
alg str | None

the Key Management algorithm to use to unwrap the CEK

None
**headers Any

additional headers used to decrypt the CEK (e.g. "epk" for ECDH algs, "iv", "tag" for AES-GCM algs)

{}

Returns:

Type Description
Jwk

the clear-text CEK, as a SymmetricJwk instance

Raises:

Type Description
UnsupportedAlg

if the requested alg identifier is not supported

Source code in jwskate/jwk/base.py
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
def recipient_key(  # noqa: C901
    self,
    wrapped_cek: bytes | SupportsBytes,
    enc: str,
    *,
    alg: str | None = None,
    **headers: Any,
) -> Jwk:
    """Produce a Content Encryption Key, to use for decryption.

    This method is meant to be used by encrypted token recipient.
    Senders should use the matching method `Jwk.sender_key()`.

    Args:
      wrapped_cek: the wrapped CEK
      enc: the encryption algorithm to use with the CEK
      alg: the Key Management algorithm to use to unwrap the CEK
      **headers: additional headers used to decrypt the CEK (e.g. "epk" for ECDH algs, "iv", "tag" for AES-GCM algs)

    Returns:
      the clear-text CEK, as a SymmetricJwk instance

    Raises:
        UnsupportedAlg: if the requested alg identifier is not supported

    """
    from jwskate import SymmetricJwk

    if not self.is_symmetric and not self.is_private:
        msg = (
            "You are using a public key for recipient key unwrapping. "
            "Key unwrapping must always be done using the recipient private key."
        )
        raise ValueError(msg)

    key_alg_wrapper = self.key_management_wrapper(alg)
    enc_alg_class = select_alg_class(SymmetricJwk.ENCRYPTION_ALGORITHMS, alg=enc)

    if isinstance(key_alg_wrapper, BaseRsaKeyWrap):
        cek = key_alg_wrapper.unwrap_key(wrapped_cek)

    elif isinstance(key_alg_wrapper, EcdhEs):
        epk = headers.get("epk")
        if epk is None:
            msg = "No EPK in the headers!"
            raise ValueError(msg)
        epk_jwk = Jwk(epk)
        if epk_jwk.is_private:
            msg = "The EPK present in the header is private."
            raise ValueError(msg)
        epk = epk_jwk.cryptography_key
        if isinstance(key_alg_wrapper, BaseEcdhEs_AesKw):
            cek = key_alg_wrapper.unwrap_key_with_epk(wrapped_cek, epk, alg=key_alg_wrapper.name)
        else:
            cek = key_alg_wrapper.recipient_key(
                epk,
                alg=enc_alg_class.name,
                key_size=enc_alg_class.key_size,
                **headers,
            )

    elif isinstance(key_alg_wrapper, BaseAesKeyWrap):
        cek = key_alg_wrapper.unwrap_key(wrapped_cek)

    elif isinstance(key_alg_wrapper, BaseAesGcmKeyWrap):
        iv = headers.get("iv")
        if iv is None:
            msg = "No 'iv' in headers!"
            raise ValueError(msg)
        iv = BinaPy(iv).decode_from("b64u")
        tag = headers.get("tag")
        if tag is None:
            msg = "No 'tag' in headers!"
            raise ValueError(msg)
        tag = BinaPy(tag).decode_from("b64u")
        cek = key_alg_wrapper.unwrap_key(wrapped_cek, tag=tag, iv=iv)

    elif isinstance(key_alg_wrapper, DirectKeyUse):
        cek = key_alg_wrapper.direct_key(enc_alg_class)

    else:
        msg = f"Unsupported Key Management Alg {key_alg_wrapper}"
        raise UnsupportedAlg(msg)  # pragma: no cover

    return SymmetricJwk.from_bytes(cek)

public_jwk

1
public_jwk() -> Jwk

Return the public Jwk associated with this key.

Returns:

Type Description
Jwk

a Jwk with the public key

Source code in jwskate/jwk/base.py
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
def public_jwk(self) -> Jwk:
    """Return the public Jwk associated with this key.

    Returns:
      a Jwk with the public key

    """
    if not self.is_private:
        return self

    params = {name: self.get(name) for name, param in self.PARAMS.items() if not param.is_private}

    if "key_ops" in self:
        key_ops = list(self.key_ops)
        if "sign" in key_ops:
            key_ops.remove("sign")
            key_ops.append("verify")
        if "unwrapKey" in key_ops:
            key_ops.remove("unwrapKey")
            key_ops.append("wrapKey")
    else:
        key_ops = None

    return Jwk(
        dict(
            kty=self.kty,
            kid=self.get("kid"),
            alg=self.get("alg"),
            use=self.get("use"),
            key_ops=key_ops,
            **params,
        )
    )

as_jwks

1
as_jwks() -> JwkSet

Return a JwkSet with this key as single element.

Returns:

Type Description
JwkSet

a JwsSet with this single key

Source code in jwskate/jwk/base.py
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
def as_jwks(self) -> JwkSet:
    """Return a JwkSet with this key as single element.

    Returns:
        a JwsSet with this single key

    """
    from .jwks import JwkSet

    return JwkSet(keys=(self,))

from_cryptography_key classmethod

1
2
3
from_cryptography_key(
    cryptography_key: Any, **kwargs: Any
) -> Jwk

Initialize a Jwk from a key from the cryptography library.

The input key can be any private or public key supported by cryptography.

Parameters:

Name Type Description Default
cryptography_key Any

a cryptography key instance

required
**kwargs Any

additional members to include in the Jwk (e.g. kid, use)

{}

Returns:

Type Description
Jwk

the matching Jwk instance

Raises:

Type Description
TypeError

if the key type is not supported

Source code in jwskate/jwk/base.py
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
@classmethod
def from_cryptography_key(cls, cryptography_key: Any, **kwargs: Any) -> Jwk:
    """Initialize a Jwk from a key from the `cryptography` library.

    The input key can be any private or public key supported by cryptography.

    Args:
      cryptography_key: a `cryptography` key instance
      **kwargs: additional members to include in the Jwk (e.g. kid, use)

    Returns:
        the matching `Jwk` instance

    Raises:
        TypeError: if the key type is not supported

    """
    for jwk_class in Jwk.__subclasses__():
        for cryptography_class in (
            jwk_class.CRYPTOGRAPHY_PRIVATE_KEY_CLASSES + jwk_class.CRYPTOGRAPHY_PUBLIC_KEY_CLASSES
        ):
            if isinstance(cryptography_key, cryptography_class):
                return jwk_class.from_cryptography_key(cryptography_key, **kwargs)

    msg = f"Unsupported Jwk class for this Key Type: {type(cryptography_key).__name__}"
    raise TypeError(msg)

_to_cryptography_key

1
_to_cryptography_key() -> Any

Return a key from the cryptography library that matches this Jwk.

This is implemented by subclasses.

Returns:

Type Description
Any

a cryptographykey instance initialized from the current key

Source code in jwskate/jwk/base.py
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
def _to_cryptography_key(self) -> Any:
    """Return a key from the `cryptography` library that matches this Jwk.

    This is implemented by subclasses.

    Returns:
        a `cryptography`key instance initialized from the current key

    """
    raise NotImplementedError

from_pem classmethod

1
2
3
4
5
from_pem(
    pem: bytes | str,
    password: bytes | str | None = None,
    **kwargs: Any
) -> Jwk

Load a Jwk from a PEM encoded private or public key.

Parameters:

Name Type Description Default
pem bytes | str

the PEM encoded data to load

required
password bytes | str | None

the password to decrypt the PEM, if required. Should be bytes. If it is a string, it will be encoded with UTF-8.

None
**kwargs Any

additional members to include in the Jwk (e.g. kid, use)

{}

Returns:

Type Description
Jwk

a Jwk instance from the loaded key

Source code in jwskate/jwk/base.py
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
@classmethod
def from_pem(
    cls,
    pem: bytes | str,
    password: bytes | str | None = None,
    **kwargs: Any,
) -> Jwk:
    """Load a `Jwk` from a PEM encoded private or public key.

    Args:
      pem: the PEM encoded data to load
      password: the password to decrypt the PEM, if required. Should be bytes.
          If it is a string, it will be encoded with UTF-8.
      **kwargs: additional members to include in the `Jwk` (e.g. `kid`, `use`)

    Returns:
        a `Jwk` instance from the loaded key

    """
    pem = pem.encode() if isinstance(pem, str) else pem
    password = password.encode("UTF-8") if isinstance(password, str) else password

    try:
        cryptography_key = serialization.load_pem_private_key(pem, password)
    except Exception as private_exc:
        try:
            cryptography_key = serialization.load_pem_public_key(pem)

        except Exception:
            msg = "The provided data is not a private or a public PEM encoded key."
            raise ValueError(msg) from private_exc
        if password is not None:
            msg = (
                "A public key was loaded from PEM, while a password was provided for decryption. "
                "Only private keys are encrypted using a password."
            )
            raise ValueError(msg) from None

    return cls.from_cryptography_key(cryptography_key, **kwargs)

to_pem

1
to_pem(password: bytes | str | None = None) -> str

Serialize this key to PEM format.

For private keys, you can provide a password for encryption. This password should be bytes. A str is also accepted, and will be encoded to bytes using UTF-8 before it is used as encryption key.

Parameters:

Name Type Description Default
password bytes | str | None

password to use to encrypt the PEM.

None

Returns:

Type Description
str

the PEM serialized key

Source code in jwskate/jwk/base.py
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
def to_pem(self, password: bytes | str | None = None) -> str:
    """Serialize this key to PEM format.

    For private keys, you can provide a password for encryption. This password should be `bytes`. A `str` is also
    accepted, and will be encoded to `bytes` using UTF-8 before it is used as encryption key.

    Args:
      password: password to use to encrypt the PEM.

    Returns:
        the PEM serialized key

    """
    password = password.encode("UTF-8") if isinstance(password, str) else password

    if self.is_private:
        encryption: serialization.KeySerializationEncryption
        encryption = serialization.BestAvailableEncryption(password) if password else serialization.NoEncryption()
        return self.cryptography_key.private_bytes(  # type: ignore[no-any-return]
            serialization.Encoding.PEM,
            serialization.PrivateFormat.PKCS8,
            encryption,
        ).decode()
    else:
        if password:
            msg = "Public keys cannot be encrypted when serialized."
            raise ValueError(msg)
        return self.cryptography_key.public_bytes(  # type: ignore[no-any-return]
            serialization.Encoding.PEM,
            serialization.PublicFormat.SubjectPublicKeyInfo,
        ).decode()

from_der classmethod

1
2
3
4
5
from_der(
    der: bytes,
    password: bytes | str | None = None,
    **kwargs: Any
) -> Jwk

Load a Jwk from DER.

Source code in jwskate/jwk/base.py
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
@classmethod
def from_der(
    cls,
    der: bytes,
    password: bytes | str | None = None,
    **kwargs: Any,
) -> Jwk:
    """Load a `Jwk` from DER."""
    password = password.encode("UTF-8") if isinstance(password, str) else password

    try:
        cryptography_key = serialization.load_der_private_key(der, password)
    except Exception as private_exc:
        try:
            cryptography_key = serialization.load_der_public_key(der)
        except Exception:
            msg = "The provided data is not a private or a public DER encoded key."
            raise ValueError(msg) from private_exc
        if password is not None:
            msg = (
                "A public key was loaded from DER, while a password was provided for decryption. "
                "Only private keys are encrypted using a password."
            )
            raise ValueError(msg) from None

    return cls.from_cryptography_key(cryptography_key, **kwargs)

to_der

1
to_der(password: bytes | str | None = None) -> BinaPy

Serialize this key to DER.

For private keys, you can provide a password for encryption. This password should be bytes. A str is also accepted, and will be encoded to bytes using UTF-8 before it is used as encryption key.

Parameters:

Name Type Description Default
password bytes | str | None

password to use to encrypt the PEM. Should be bytes. If it is a string, it will be encoded to bytes with UTF-8.

None

Returns:

Type Description
BinaPy

the DER serialized key

Source code in jwskate/jwk/base.py
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
def to_der(self, password: bytes | str | None = None) -> BinaPy:
    """Serialize this key to DER.

    For private keys, you can provide a password for encryption. This password should be bytes. A `str` is also
    accepted, and will be encoded to `bytes` using UTF-8 before it is used as encryption key.

    Args:
      password: password to use to encrypt the PEM. Should be bytes.
        If it is a string, it will be encoded to bytes with UTF-8.

    Returns:
        the DER serialized key

    """
    password = password.encode("UTF-8") if isinstance(password, str) else password

    if self.is_private:
        encryption: serialization.KeySerializationEncryption
        encryption = serialization.BestAvailableEncryption(password) if password else serialization.NoEncryption()
        return BinaPy(
            self.cryptography_key.private_bytes(
                serialization.Encoding.DER,
                serialization.PrivateFormat.PKCS8,
                encryption,
            )
        )
    else:
        if password:
            msg = "Public keys cannot be encrypted when serialized."
            raise ValueError(msg)
        return BinaPy(
            self.cryptography_key.public_bytes(
                serialization.Encoding.DER,
                serialization.PublicFormat.SubjectPublicKeyInfo,
            )
        )

from_x509 classmethod

1
from_x509(x509_pem: str | bytes) -> Self

Read the public key from a X509 certificate, PEM formatted.

Source code in jwskate/jwk/base.py
1186
1187
1188
1189
1190
1191
1192
1193
@classmethod
def from_x509(cls, x509_pem: str | bytes) -> Self:
    """Read the public key from a X509 certificate, PEM formatted."""
    if isinstance(x509_pem, str):
        x509_pem = x509_pem.encode()

    cert = x509.load_pem_x509_certificate(x509_pem)
    return cls(cert.public_key())

generate classmethod

1
2
3
4
5
6
generate(
    *,
    alg: str | None = None,
    kty: str | None = None,
    **kwargs: Any
) -> Jwk

Generate a Private Key and return it as a Jwk instance.

This method is implemented by subclasses for specific Key Types and returns an instance of that subclass.

Parameters:

Name Type Description Default
alg str | None

intended algorithm to use with the generated key

None
kty str | None

key type identifier

None
**kwargs Any

specific parameters depending on the type of key, or additional members to include in the Jwk

{}

Returns:

Type Description
Jwk

a Jwk instance with a generated key

Source code in jwskate/jwk/base.py
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
@classmethod
def generate(cls, *, alg: str | None = None, kty: str | None = None, **kwargs: Any) -> Jwk:
    """Generate a Private Key and return it as a `Jwk` instance.

    This method is implemented by subclasses for specific Key Types and returns an instance of that subclass.

    Args:
        alg: intended algorithm to use with the generated key
        kty: key type identifier
        **kwargs: specific parameters depending on the type of key, or additional members to include in the `Jwk`

    Returns:
        a `Jwk` instance with a generated key

    """
    if alg:
        key = cls.generate_for_alg(alg=alg, **kwargs)
        if kty is not None and key.kty != kty:
            msg = f"Incompatible `{alg=}` and `{kty=}` parameters. `{alg=}` points to `kty='{key.kty}'`."
            raise ValueError(msg)
        return key
    if kty:
        return cls.generate_for_kty(kty=kty, **kwargs)
    msg = (
        "You must provide a hint for jwskate to know what kind of key it must generate. "
        "You can either provide an 'alg' identifier as keyword parameter, and/or a 'kty'."
    )
    raise ValueError(msg)

copy

1
copy() -> Jwk

Create a copy of this key.

Returns:

Type Description
Jwk

a copy of this key, with the same value

Source code in jwskate/jwk/base.py
1224
1225
1226
1227
1228
1229
1230
1231
def copy(self) -> Jwk:
    """Create a copy of this key.

    Returns:
        a copy of this key, with the same value

    """
    return Jwk(copy(self.data))

with_kid_thumbprint

1
with_kid_thumbprint(*, force: bool = False) -> Jwk

Include the JWK thumbprint as kid.

If key already has a kid (Key ID):

  • if force is True, this erases the previous "kid".
  • if force is False (default), do nothing.

Parameters:

Name Type Description Default
force bool

whether to overwrite a previously existing kid

False

Returns:

Type Description
Jwk

a copy of this Jwk, with a kid attribute.

Source code in jwskate/jwk/base.py
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
def with_kid_thumbprint(self, *, force: bool = False) -> Jwk:
    """Include the JWK thumbprint as `kid`.

    If key already has a `kid` (Key ID):

    - if `force` is `True`, this erases the previous "kid".
    - if `force` is `False` (default), do nothing.

    Args:
        force: whether to overwrite a previously existing kid

    Returns:
        a copy of this Jwk, with a `kid` attribute.

    """
    jwk = self.copy()
    if self.get("kid") is not None and not force:
        return jwk
    jwk["kid"] = self.thumbprint()
    return jwk

with_usage_parameters

1
2
3
4
5
6
7
with_usage_parameters(
    alg: str | None = None,
    *,
    with_alg: bool = True,
    with_use: bool = True,
    with_key_ops: bool = True
) -> Jwk

Copy this Jwk and add the use and key_ops parameters.

The returned jwk alg parameter will be the one passed as parameter to this method, or as default the one declared as alg parameter in this Jwk.

The use (Public Key Use) param is deduced based on this alg value.

The key_ops (Key Operations) param is deduced based on the key use and if the key is public, private, or symmetric.

Parameters:

Name Type Description Default
alg str | None

the alg to use, if not present in this Jwk

None
with_alg bool

whether to include an alg parameter

True
with_use bool

whether to include a use parameter

True
with_key_ops bool

whether to include a key_ops parameter

True

Returns:

Type Description
Jwk

a Jwk with the same key, with alg, use and key_ops parameters.

Source code in jwskate/jwk/base.py
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
def with_usage_parameters(
    self,
    alg: str | None = None,
    *,
    with_alg: bool = True,
    with_use: bool = True,
    with_key_ops: bool = True,
) -> Jwk:
    """Copy this Jwk and add the `use` and `key_ops` parameters.

    The returned jwk `alg` parameter will be the one passed as parameter to this method,
    or as default the one declared as `alg` parameter in this Jwk.

    The `use` (Public Key Use) param is deduced based on this `alg` value.

    The `key_ops` (Key Operations) param is deduced based on the key `use` and if the key is public, private,
    or symmetric.

    Args:
        alg: the alg to use, if not present in this Jwk
        with_alg: whether to include an `alg` parameter
        with_use: whether to include a `use` parameter
        with_key_ops: whether to include a `key_ops` parameter

    Returns:
        a Jwk with the same key, with `alg`, `use` and `key_ops` parameters.

    """
    alg = alg or self.alg

    if not alg:
        msg = "An algorithm is required to set the usage parameters"
        raise ExpectedAlgRequired(msg)

    self._get_alg_class(alg)  # raises an exception if alg is not supported

    jwk = self.copy()
    if with_alg:
        jwk["alg"] = alg
    if with_use:
        jwk["use"] = jwk.use
    if with_key_ops:
        jwk["key_ops"] = jwk.key_ops

    return jwk

minimize

1
minimize() -> Jwk

Strip out any optional or non-standard parameter from that key.

This will remove alg, use, key_ops, optional parameters from RSA keys, and other unknown parameters.

Source code in jwskate/jwk/base.py
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
def minimize(self) -> Jwk:
    """Strip out any optional or non-standard parameter from that key.

    This will remove `alg`, `use`, `key_ops`, optional parameters from RSA keys, and other
    unknown parameters.

    """
    jwk = self.copy()
    for key in self.keys():
        if key == "kty" or key in self.PARAMS and self.PARAMS[key].is_required:
            continue
        del jwk[key]

    return jwk

__eq__

1
__eq__(other: Any) -> bool

Compare JWK keys, ignoring optional/informational fields.

Source code in jwskate/jwk/base.py
1315
1316
1317
1318
def __eq__(self, other: Any) -> bool:
    """Compare JWK keys, ignoring optional/informational fields."""
    other = to_jwk(other)
    return super(Jwk, self.minimize()).__eq__(other.minimize())

UnsupportedKeyType

Bases: ValueError

Raised when an unsupported Key Type is requested.

Source code in jwskate/jwk/base.py
46
47
class UnsupportedKeyType(ValueError):
    """Raised when an unsupported Key Type is requested."""

ECJwk

Bases: Jwk

Represent an Elliptic Curve key in JWK format.

Elliptic Curve keys have Key Type "EC".

Source code in jwskate/jwk/ec.py
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 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
132
133
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
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
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
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
class ECJwk(Jwk):
    """Represent an Elliptic Curve key in JWK format.

    Elliptic Curve keys have Key Type `"EC"`.

    """

    KTY = KeyTypes.EC

    CRYPTOGRAPHY_PRIVATE_KEY_CLASSES = (ec.EllipticCurvePrivateKey,)

    CRYPTOGRAPHY_PUBLIC_KEY_CLASSES = (ec.EllipticCurvePublicKey,)

    PARAMS: Mapping[str, JwkParameter] = {
        "crv": JwkParameter("Curve", is_private=False, is_required=True, kind="name"),
        "x": JwkParameter("X Coordinate", is_private=False, is_required=True, kind="b64u"),
        "y": JwkParameter("Y Coordinate", is_private=False, is_required=True, kind="b64u"),
        "d": JwkParameter("ECC Private Key", is_private=True, is_required=True, kind="b64u"),
    }

    CURVES: Mapping[str, EllipticCurve] = {curve.name: curve for curve in [P_256, P_384, P_521, secp256k1]}

    SIGNATURE_ALGORITHMS: Mapping[str, type[BaseECSignatureAlg]] = {
        sigalg.name: sigalg for sigalg in [ES256, ES384, ES512, ES256K]
    }

    KEY_MANAGEMENT_ALGORITHMS: Mapping[str, type[EcdhEs]] = {
        keyalg.name: keyalg for keyalg in [EcdhEs, EcdhEs_A128KW, EcdhEs_A192KW, EcdhEs_A256KW]
    }

    @property
    @override
    def is_private(self) -> bool:
        return "d" in self

    @override
    def _validate(self) -> None:
        self.get_curve(self.crv)
        super()._validate()

    @classmethod
    def get_curve(cls, crv: str) -> EllipticCurve:
        """Get the EllipticCurve instance for a given curve identifier.

        Args:
          crv: the curve identifier

        Returns:
            the matching `EllipticCurve` instance

        Raises:
            UnsupportedEllipticCurve: if the curve identifier is not supported

        """
        curve = cls.CURVES.get(crv)
        if curve is None:
            raise UnsupportedEllipticCurve(crv)
        return curve

    @property
    def curve(self) -> EllipticCurve:
        """Get the `EllipticCurve` instance for this key.

        Returns:
            the `EllipticCurve` instance

        """
        return self.get_curve(self.crv)

    @classmethod
    def public(cls, *, crv: str, x: int, y: int, **params: str) -> ECJwk:
        """Initialize a public `ECJwk` from its public parameters.

        Args:
          crv: the curve to use
          x: the x coordinate
          y: the y coordinate
          **params: additional member to include in the Jwk

        Returns:
          an ECJwk initialized with the supplied parameters

        """
        coord_size = cls.get_curve(crv).coordinate_size
        return cls(
            dict(
                kty=cls.KTY,
                crv=crv,
                x=BinaPy.from_int(x, length=coord_size).to("b64u").ascii(),
                y=BinaPy.from_int(y, length=coord_size).to("b64u").ascii(),
                **{k: v for k, v in params.items() if v is not None},
            )
        )

    @classmethod
    def private(cls, *, crv: str, x: int, y: int, d: int, **params: Any) -> ECJwk:
        """Initialize a private ECJwk from its private parameters.

        Args:
          crv: the curve to use
          x: the x coordinate
          y: the y coordinate
          d: the elliptic curve private key
          **params: additional members to include in the JWK

        Returns:
          an ECJwk initialized with the supplied parameters

        """
        coord_size = cls.get_curve(crv).coordinate_size
        return cls(
            dict(
                kty=cls.KTY,
                crv=crv,
                x=BinaPy.from_int(x, length=coord_size).to("b64u").ascii(),
                y=BinaPy.from_int(y, length=coord_size).to("b64u").ascii(),
                d=BinaPy.from_int(d, length=coord_size).to("b64u").ascii(),
                **{k: v for k, v in params.items() if v is not None},
            )
        )

    @classmethod
    @override
    def generate(cls, *, crv: str | None = None, alg: str | None = None, **kwargs: Any) -> ECJwk:
        curve: EllipticCurve = P_256

        if crv is None and alg is None:
            msg = (
                "No Curve identifier (crv) or Algorithm identifier (alg) have been provided "
                "when generating an Elliptic Curve JWK. So there is no hint to determine which curve to use. "
                "You must explicitly pass an 'alg' or 'crv' parameter to select the appropriate Curve."
            )
            raise ValueError(msg)
        elif crv:
            curve = cls.get_curve(crv)
        elif alg:
            if alg in cls.SIGNATURE_ALGORITHMS:
                curve = cls.SIGNATURE_ALGORITHMS[alg].curve
            elif alg in cls.KEY_MANAGEMENT_ALGORITHMS:
                warnings.warn(
                    "No Curve identifier (crv) specified when generating an Elliptic Curve Jwk for Key Management. "
                    "Curve 'P-256' is used by default. You should explicitly pass a 'crv' parameter "
                    "to select the appropriate Curve and avoid this warning.",
                    stacklevel=2,
                )
            else:
                raise UnsupportedAlg(alg)

        key = ec.generate_private_key(curve.cryptography_curve)
        pn = key.private_numbers()  # type: ignore[attr-defined]
        x = pn.public_numbers.x
        y = pn.public_numbers.y
        d = pn.private_value

        return cls.private(
            crv=curve.name,
            alg=alg,
            x=x,
            y=y,
            d=d,
            **kwargs,
        )

    @classmethod
    @override
    def from_cryptography_key(cls, cryptography_key: Any, **kwargs: Any) -> ECJwk:
        public_numbers: ec.EllipticCurvePublicNumbers
        if isinstance(cryptography_key, ec.EllipticCurvePrivateKey):
            public_numbers = cryptography_key.public_key().public_numbers()
        elif isinstance(cryptography_key, ec.EllipticCurvePublicKey):
            public_numbers = cryptography_key.public_numbers()
        else:
            msg = "A EllipticCurvePrivateKey or a EllipticCurvePublicKey is required."
            raise TypeError(msg)

        for crv in EllipticCurve.instances.values():
            if crv.cryptography_curve.name == cryptography_key.curve.name:
                break
        else:
            msg = f"Unsupported Curve {cryptography_key.curve.name}"
            raise NotImplementedError(msg)

        x = BinaPy.from_int(public_numbers.x, length=crv.coordinate_size).to("b64u").ascii()
        y = BinaPy.from_int(public_numbers.y, length=crv.coordinate_size).to("b64u").ascii()
        parameters = {"kty": KeyTypes.EC, "crv": crv.name, "x": x, "y": y}
        if isinstance(cryptography_key, ec.EllipticCurvePrivateKey):
            pn = cryptography_key.private_numbers()  # type: ignore[attr-defined]
            d = BinaPy.from_int(pn.private_value, length=crv.coordinate_size).to("b64u").ascii()
            parameters["d"] = d

        return cls(parameters)

    @override
    def _to_cryptography_key(
        self,
    ) -> ec.EllipticCurvePrivateKey | ec.EllipticCurvePublicKey:
        if self.is_private:
            return ec.EllipticCurvePrivateNumbers(
                private_value=self.ecc_private_key,
                public_numbers=ec.EllipticCurvePublicNumbers(
                    x=self.x_coordinate,
                    y=self.y_coordinate,
                    curve=self.curve.cryptography_curve,
                ),
            ).private_key()
        else:
            return ec.EllipticCurvePublicNumbers(
                x=self.x_coordinate,
                y=self.y_coordinate,
                curve=self.curve.cryptography_curve,
            ).public_key()

    @property
    def coordinate_size(self) -> int:
        """The coordinate size to use with the key curve.

        This is 32, 48, or 66 bits.

        """
        return self.curve.coordinate_size

    @cached_property
    def x_coordinate(self) -> int:
        """Return the *x coordinate*, parameter `x` from this `ECJwk`."""
        return BinaPy(self.x).decode_from("b64u").to_int()

    @cached_property
    def y_coordinate(self) -> int:
        """Return the *y coordinate*, parameter `y` from this `ECJwk`."""
        return BinaPy(self.y).decode_from("b64u").to_int()

    @cached_property
    def ecc_private_key(self) -> int:
        """Return the *ECC private key*, parameter `d` from this `ECJwk`."""
        return BinaPy(self.d).decode_from("b64u").to_int()

    @override
    def supported_signing_algorithms(self) -> list[str]:
        return [name for name, alg in self.SIGNATURE_ALGORITHMS.items() if alg.curve == self.curve]

    @override
    def supported_key_management_algorithms(self) -> list[str]:
        return list(self.KEY_MANAGEMENT_ALGORITHMS)

    @override
    def supported_encryption_algorithms(self) -> list[str]:
        return list(self.ENCRYPTION_ALGORITHMS)

curve property

1
curve: EllipticCurve

Get the EllipticCurve instance for this key.

Returns:

Type Description
EllipticCurve

the EllipticCurve instance

coordinate_size property

1
coordinate_size: int

The coordinate size to use with the key curve.

This is 32, 48, or 66 bits.

x_coordinate cached property

1
x_coordinate: int

Return the x coordinate, parameter x from this ECJwk.

y_coordinate cached property

1
y_coordinate: int

Return the y coordinate, parameter y from this ECJwk.

ecc_private_key cached property

1
ecc_private_key: int

Return the ECC private key, parameter d from this ECJwk.

get_curve classmethod

1
get_curve(crv: str) -> EllipticCurve

Get the EllipticCurve instance for a given curve identifier.

Parameters:

Name Type Description Default
crv str

the curve identifier

required

Returns:

Type Description
EllipticCurve

the matching EllipticCurve instance

Raises:

Type Description
UnsupportedEllipticCurve

if the curve identifier is not supported

Source code in jwskate/jwk/ec.py
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
@classmethod
def get_curve(cls, crv: str) -> EllipticCurve:
    """Get the EllipticCurve instance for a given curve identifier.

    Args:
      crv: the curve identifier

    Returns:
        the matching `EllipticCurve` instance

    Raises:
        UnsupportedEllipticCurve: if the curve identifier is not supported

    """
    curve = cls.CURVES.get(crv)
    if curve is None:
        raise UnsupportedEllipticCurve(crv)
    return curve

public classmethod

1
public(*, crv: str, x: int, y: int, **params: str) -> ECJwk

Initialize a public ECJwk from its public parameters.

Parameters:

Name Type Description Default
crv str

the curve to use

required
x int

the x coordinate

required
y int

the y coordinate

required
**params str

additional member to include in the Jwk

{}

Returns:

Type Description
ECJwk

an ECJwk initialized with the supplied parameters

Source code in jwskate/jwk/ec.py
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
@classmethod
def public(cls, *, crv: str, x: int, y: int, **params: str) -> ECJwk:
    """Initialize a public `ECJwk` from its public parameters.

    Args:
      crv: the curve to use
      x: the x coordinate
      y: the y coordinate
      **params: additional member to include in the Jwk

    Returns:
      an ECJwk initialized with the supplied parameters

    """
    coord_size = cls.get_curve(crv).coordinate_size
    return cls(
        dict(
            kty=cls.KTY,
            crv=crv,
            x=BinaPy.from_int(x, length=coord_size).to("b64u").ascii(),
            y=BinaPy.from_int(y, length=coord_size).to("b64u").ascii(),
            **{k: v for k, v in params.items() if v is not None},
        )
    )

private classmethod

1
2
3
private(
    *, crv: str, x: int, y: int, d: int, **params: Any
) -> ECJwk

Initialize a private ECJwk from its private parameters.

Parameters:

Name Type Description Default
crv str

the curve to use

required
x int

the x coordinate

required
y int

the y coordinate

required
d int

the elliptic curve private key

required
**params Any

additional members to include in the JWK

{}

Returns:

Type Description
ECJwk

an ECJwk initialized with the supplied parameters

Source code in jwskate/jwk/ec.py
133
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
@classmethod
def private(cls, *, crv: str, x: int, y: int, d: int, **params: Any) -> ECJwk:
    """Initialize a private ECJwk from its private parameters.

    Args:
      crv: the curve to use
      x: the x coordinate
      y: the y coordinate
      d: the elliptic curve private key
      **params: additional members to include in the JWK

    Returns:
      an ECJwk initialized with the supplied parameters

    """
    coord_size = cls.get_curve(crv).coordinate_size
    return cls(
        dict(
            kty=cls.KTY,
            crv=crv,
            x=BinaPy.from_int(x, length=coord_size).to("b64u").ascii(),
            y=BinaPy.from_int(y, length=coord_size).to("b64u").ascii(),
            d=BinaPy.from_int(d, length=coord_size).to("b64u").ascii(),
            **{k: v for k, v in params.items() if v is not None},
        )
    )

UnsupportedEllipticCurve

Bases: KeyError

Raised when an unsupported Elliptic Curve is requested.

Source code in jwskate/jwk/ec.py
35
36
class UnsupportedEllipticCurve(KeyError):
    """Raised when an unsupported Elliptic Curve is requested."""

JwkSet

Bases: BaseJsonDict

A set of JWK keys, with methods for easy management of keys.

A JwkSet is a dict subclass, so you can do anything with a JwkSet that you can do with a dict. In addition, it provides a few helpers methods to get the keys, add or remove keys, and verify signatures using keys from this set.

  • a dict from the parsed JSON object representing this JwkSet (in parameter jwks)
  • a list of Jwk (in parameter keys)
  • nothing, to initialize an empty JwkSet

Parameters:

Name Type Description Default
jwks Mapping[str, Any] | None

a dict, containing the JwkSet, parsed as a JSON object.

None
keys Iterable[Jwk | Mapping[str, Any]] | None

a list of Jwk, that will be added to this JwkSet

None
Source code in jwskate/jwk/jwks.py
 14
 15
 16
 17
 18
 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
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 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
132
133
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
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
class JwkSet(BaseJsonDict):
    """A set of JWK keys, with methods for easy management of keys.

    A JwkSet is a dict subclass, so you can do anything with a JwkSet
    that you can do with a dict. In addition, it provides a few helpers
    methods to get the keys, add or remove keys, and verify signatures
    using keys from this set.

    - a `dict` from the parsed JSON object representing this JwkSet (in parameter `jwks`)
    - a list of `Jwk` (in parameter `keys`)
    - nothing, to initialize an empty JwkSet

    Args:
        jwks: a dict, containing the JwkSet, parsed as a JSON object.
        keys: a list of `Jwk`, that will be added to this JwkSet

    """

    def __init__(
        self,
        jwks: Mapping[str, Any] | None = None,
        keys: Iterable[Jwk | Mapping[str, Any]] | None = None,
    ):
        super().__init__({k: v for k, v in jwks.items() if k != "keys"} if jwks else {})
        if keys is None and jwks is not None and "keys" in jwks:
            keys = jwks.get("keys")
        if keys:
            for key in keys:
                self.add_jwk(key)

    @override
    def __setitem__(self, name: str, value: Any) -> None:
        if name == "keys":
            for key in value:
                self.add_jwk(key)
        else:
            super().__setitem__(name, value)

    @property
    def jwks(self) -> list[Jwk]:
        """Return the list of keys from this JwkSet, as `Jwk` instances.

        Returns:
            a list of `Jwk`

        """
        return self.get("keys", [])

    def get_jwk_by_kid(self, kid: str) -> Jwk:
        """Return a Jwk from this JwkSet, based on its kid.

        Args:
          kid: the kid of the key to obtain

        Returns:
            the key with the matching Key ID

        Raises:
            KeyError: if no key matches

        """
        jwk = next(filter(lambda j: j.get("kid") == kid, self.jwks), None)
        if isinstance(jwk, Jwk):
            return jwk
        raise KeyError(kid)

    def __len__(self) -> int:
        """Return the number of Jwk in this JwkSet.

        Returns:
            the number of keys

        """
        return len(self.jwks)

    def add_jwk(
        self,
        key: Jwk | Mapping[str, Any] | Any,
    ) -> str:
        """Add a Jwk in this JwkSet.

        Args:
          key: the Jwk to add (either a `Jwk` instance, or a dict containing the Jwk parameters)

        Returns:
          the key ID. It will be generated if missing from the given Jwk.

        """
        key = to_jwk(key).with_kid_thumbprint()

        self.data.setdefault("keys", [])
        self.data["keys"].append(key)

        return key.kid

    def remove_jwk(self, kid: str) -> None:
        """Remove a Jwk from this JwkSet, based on a `kid`.

        Args:
          kid: the `kid` from the key to be removed.

        Raises:
            KeyError: if no key matches

        """
        try:
            jwk = self.get_jwk_by_kid(kid)
            self.jwks.remove(jwk)
        except KeyError:
            pass

    @property
    def is_private(self) -> bool:
        """True if the JwkSet contains at least one private key.

        Returns:
            `True` if this JwkSet contains at least one private key

        """
        return any(key.is_private for key in self.jwks)

    def public_jwks(self) -> JwkSet:
        """Return another JwkSet with the public keys associated with the current keys.

        Returns:
            a public JwkSet

        """
        return JwkSet(keys=(key.public_jwk() for key in self.jwks))

    def verification_keys(self) -> list[Jwk]:
        """Return the list of keys from this JWKS that are usable for signature verification.

        To be usable for signature verification, a key must:

        - be asymmetric
        - be public
        - be flagged for signature, either with `use=sig` or an `alg` that is compatible with signature

        Returns:
            a list of `Jwk` that are usable for signature verification

        """
        return [jwk for jwk in self.jwks if not jwk.is_symmetric and not jwk.is_private and jwk.use == "sig"]

    def verify(
        self,
        data: bytes,
        signature: bytes,
        alg: str | None = None,
        algs: Iterable[str] | None = None,
        kid: str | None = None,
    ) -> bool:
        """Verify a signature with the keys from this key set.

        If a `kid` is provided, only that Key ID will be tried. Otherwise, all keys that are compatible with the
        specified alg(s) will be tried.

        Args:
          data: the signed data to verify
          signature: the signature to verify against the signed data
          alg: alg to verify the signature, if there is only 1
          algs: list of allowed signature algs, if there are several
          kid: the kid of the Jwk that will be used to validate the signature. If no kid is provided, multiple keys
            from this key set may be tried.

        Returns:
          `True` if the signature validates with any of the tried keys, `False` otherwise

        """
        if not alg and not algs:
            msg = "Please provide either 'alg' or 'algs' parameter"
            raise ValueError(msg)

        # if a kid is provided, try only the key matching `kid`
        if kid is not None:
            jwk = self.get_jwk_by_kid(kid)
            return jwk.verify(data, signature, alg=alg, algs=algs)

        # otherwise, try all keys that support the given alg(s)
        if algs is None:
            if alg is not None:
                algs = (alg,)
        else:
            algs = list(algs)

        for jwk in self.verification_keys():
            for alg in algs or (None,):
                if alg in jwk.supported_signing_algorithms() and jwk.verify(data, signature, alg=alg):
                    return True

        # no key matches, so consider the signature invalid
        return False

    def encryption_keys(self) -> list[Jwk]:
        """Return the list of keys from this JWKS that are usable for encryption.

        To be usable for encryption, a key must:

        - be asymmetric
        - be public
        - be flagged for encryption, either with `use=enc` or an `alg` parameter that is an encryption alg

        Returns:
            a list of `Jwk` that are suitable for encryption

        """
        return [jwk for jwk in self.jwks if not jwk.is_symmetric and not jwk.is_private and jwk.use == "enc"]

jwks property

1
jwks: list[Jwk]

Return the list of keys from this JwkSet, as Jwk instances.

Returns:

Type Description
list[Jwk]

a list of Jwk

is_private property

1
is_private: bool

True if the JwkSet contains at least one private key.

Returns:

Type Description
bool

True if this JwkSet contains at least one private key

get_jwk_by_kid

1
get_jwk_by_kid(kid: str) -> Jwk

Return a Jwk from this JwkSet, based on its kid.

Parameters:

Name Type Description Default
kid str

the kid of the key to obtain

required

Returns:

Type Description
Jwk

the key with the matching Key ID

Raises:

Type Description
KeyError

if no key matches

Source code in jwskate/jwk/jwks.py
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
def get_jwk_by_kid(self, kid: str) -> Jwk:
    """Return a Jwk from this JwkSet, based on its kid.

    Args:
      kid: the kid of the key to obtain

    Returns:
        the key with the matching Key ID

    Raises:
        KeyError: if no key matches

    """
    jwk = next(filter(lambda j: j.get("kid") == kid, self.jwks), None)
    if isinstance(jwk, Jwk):
        return jwk
    raise KeyError(kid)

__len__

1
__len__() -> int

Return the number of Jwk in this JwkSet.

Returns:

Type Description
int

the number of keys

Source code in jwskate/jwk/jwks.py
80
81
82
83
84
85
86
87
def __len__(self) -> int:
    """Return the number of Jwk in this JwkSet.

    Returns:
        the number of keys

    """
    return len(self.jwks)

add_jwk

1
add_jwk(key: Jwk | Mapping[str, Any] | Any) -> str

Add a Jwk in this JwkSet.

Parameters:

Name Type Description Default
key Jwk | Mapping[str, Any] | Any

the Jwk to add (either a Jwk instance, or a dict containing the Jwk parameters)

required

Returns:

Type Description
str

the key ID. It will be generated if missing from the given Jwk.

Source code in jwskate/jwk/jwks.py
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
def add_jwk(
    self,
    key: Jwk | Mapping[str, Any] | Any,
) -> str:
    """Add a Jwk in this JwkSet.

    Args:
      key: the Jwk to add (either a `Jwk` instance, or a dict containing the Jwk parameters)

    Returns:
      the key ID. It will be generated if missing from the given Jwk.

    """
    key = to_jwk(key).with_kid_thumbprint()

    self.data.setdefault("keys", [])
    self.data["keys"].append(key)

    return key.kid

remove_jwk

1
remove_jwk(kid: str) -> None

Remove a Jwk from this JwkSet, based on a kid.

Parameters:

Name Type Description Default
kid str

the kid from the key to be removed.

required

Raises:

Type Description
KeyError

if no key matches

Source code in jwskate/jwk/jwks.py
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
def remove_jwk(self, kid: str) -> None:
    """Remove a Jwk from this JwkSet, based on a `kid`.

    Args:
      kid: the `kid` from the key to be removed.

    Raises:
        KeyError: if no key matches

    """
    try:
        jwk = self.get_jwk_by_kid(kid)
        self.jwks.remove(jwk)
    except KeyError:
        pass

public_jwks

1
public_jwks() -> JwkSet

Return another JwkSet with the public keys associated with the current keys.

Returns:

Type Description
JwkSet

a public JwkSet

Source code in jwskate/jwk/jwks.py
135
136
137
138
139
140
141
142
def public_jwks(self) -> JwkSet:
    """Return another JwkSet with the public keys associated with the current keys.

    Returns:
        a public JwkSet

    """
    return JwkSet(keys=(key.public_jwk() for key in self.jwks))

verification_keys

1
verification_keys() -> list[Jwk]

Return the list of keys from this JWKS that are usable for signature verification.

To be usable for signature verification, a key must:

  • be asymmetric
  • be public
  • be flagged for signature, either with use=sig or an alg that is compatible with signature

Returns:

Type Description
list[Jwk]

a list of Jwk that are usable for signature verification

Source code in jwskate/jwk/jwks.py
144
145
146
147
148
149
150
151
152
153
154
155
156
157
def verification_keys(self) -> list[Jwk]:
    """Return the list of keys from this JWKS that are usable for signature verification.

    To be usable for signature verification, a key must:

    - be asymmetric
    - be public
    - be flagged for signature, either with `use=sig` or an `alg` that is compatible with signature

    Returns:
        a list of `Jwk` that are usable for signature verification

    """
    return [jwk for jwk in self.jwks if not jwk.is_symmetric and not jwk.is_private and jwk.use == "sig"]

verify

1
2
3
4
5
6
7
verify(
    data: bytes,
    signature: bytes,
    alg: str | None = None,
    algs: Iterable[str] | None = None,
    kid: str | None = None,
) -> bool

Verify a signature with the keys from this key set.

If a kid is provided, only that Key ID will be tried. Otherwise, all keys that are compatible with the specified alg(s) will be tried.

Parameters:

Name Type Description Default
data bytes

the signed data to verify

required
signature bytes

the signature to verify against the signed data

required
alg str | None

alg to verify the signature, if there is only 1

None
algs Iterable[str] | None

list of allowed signature algs, if there are several

None
kid str | None

the kid of the Jwk that will be used to validate the signature. If no kid is provided, multiple keys from this key set may be tried.

None

Returns:

Type Description
bool

True if the signature validates with any of the tried keys, False otherwise

Source code in jwskate/jwk/jwks.py
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
def verify(
    self,
    data: bytes,
    signature: bytes,
    alg: str | None = None,
    algs: Iterable[str] | None = None,
    kid: str | None = None,
) -> bool:
    """Verify a signature with the keys from this key set.

    If a `kid` is provided, only that Key ID will be tried. Otherwise, all keys that are compatible with the
    specified alg(s) will be tried.

    Args:
      data: the signed data to verify
      signature: the signature to verify against the signed data
      alg: alg to verify the signature, if there is only 1
      algs: list of allowed signature algs, if there are several
      kid: the kid of the Jwk that will be used to validate the signature. If no kid is provided, multiple keys
        from this key set may be tried.

    Returns:
      `True` if the signature validates with any of the tried keys, `False` otherwise

    """
    if not alg and not algs:
        msg = "Please provide either 'alg' or 'algs' parameter"
        raise ValueError(msg)

    # if a kid is provided, try only the key matching `kid`
    if kid is not None:
        jwk = self.get_jwk_by_kid(kid)
        return jwk.verify(data, signature, alg=alg, algs=algs)

    # otherwise, try all keys that support the given alg(s)
    if algs is None:
        if alg is not None:
            algs = (alg,)
    else:
        algs = list(algs)

    for jwk in self.verification_keys():
        for alg in algs or (None,):
            if alg in jwk.supported_signing_algorithms() and jwk.verify(data, signature, alg=alg):
                return True

    # no key matches, so consider the signature invalid
    return False

encryption_keys

1
encryption_keys() -> list[Jwk]

Return the list of keys from this JWKS that are usable for encryption.

To be usable for encryption, a key must:

  • be asymmetric
  • be public
  • be flagged for encryption, either with use=enc or an alg parameter that is an encryption alg

Returns:

Type Description
list[Jwk]

a list of Jwk that are suitable for encryption

Source code in jwskate/jwk/jwks.py
208
209
210
211
212
213
214
215
216
217
218
219
220
221
def encryption_keys(self) -> list[Jwk]:
    """Return the list of keys from this JWKS that are usable for encryption.

    To be usable for encryption, a key must:

    - be asymmetric
    - be public
    - be flagged for encryption, either with `use=enc` or an `alg` parameter that is an encryption alg

    Returns:
        a list of `Jwk` that are suitable for encryption

    """
    return [jwk for jwk in self.jwks if not jwk.is_symmetric and not jwk.is_private and jwk.use == "enc"]

SymmetricJwk

Bases: Jwk

Represent a Symmetric key in JWK format.

Symmetric keys have key type "oct".

Source code in jwskate/jwk/oct.py
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 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
132
133
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
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
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
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
class SymmetricJwk(Jwk):
    """Represent a Symmetric key in JWK format.

    Symmetric keys have key type `"oct"`.

    """

    KTY = KeyTypes.OCT
    CRYPTOGRAPHY_PRIVATE_KEY_CLASSES = (bytes,)
    CRYPTOGRAPHY_PUBLIC_KEY_CLASSES = (bytes,)

    PARAMS = {
        "k": JwkParameter("Key Value", is_private=True, is_required=True, kind="b64u"),
    }

    SIGNATURE_ALGORITHMS = {sigalg.name: sigalg for sigalg in [HS256, HS384, HS512]}

    KEY_MANAGEMENT_ALGORITHMS = {
        keyalg.name: keyalg
        for keyalg in [
            A128KW,
            A192KW,
            A256KW,
            A128GCMKW,
            A192GCMKW,
            A256GCMKW,
            DirectKeyUse,
        ]
    }

    ENCRYPTION_ALGORITHMS = {
        keyalg.name: keyalg
        for keyalg in [
            A128CBC_HS256,
            A192CBC_HS384,
            A256CBC_HS512,
            A128GCM,
            A192GCM,
            A256GCM,
        ]
    }

    @property
    @override
    def is_symmetric(self) -> bool:
        return True

    @override
    def public_jwk(self) -> Jwk:
        """Raise an error since Symmetric Keys are always private.

        Raises:
            ValueError: symmetric keys are always private, it makes no sense to use them as public keys

        """
        msg = "Symmetric keys don't have a public key"
        raise ValueError(msg)

    @classmethod
    def from_bytes(cls, k: bytes | str, **params: Any) -> SymmetricJwk:
        """Initialize a `SymmetricJwk` from a raw secret key.

        The provided secret key is encoded and used as the `k` parameter for the returned `SymmetricKey`.

        Args:
          k: the key to use
          **params: additional members to include in the `Jwk`

        Returns:
          the resulting `SymmetricJwk`

        """
        return cls(dict(kty=cls.KTY, k=BinaPy(k).to("b64u").ascii(), **params))

    @classmethod
    @override
    def generate(cls, *, alg: str | None = None, key_size: int | None = None, **params: Any) -> SymmetricJwk:
        if alg:
            alg_class = cls._get_alg_class(alg)
            # special cases for AES or HMAC based algs which require a specific key size
            if issubclass(alg_class, (BaseAESEncryptionAlg, BaseAesKeyWrap)):
                if key_size is not None and key_size != alg_class.key_size:
                    msg = (
                        f"Key for {alg} must be exactly {alg_class.key_size} bits. "
                        "You should remove the `key_size` parameter to generate a key of the appropriate length."
                    )
                    raise ValueError(msg)
                key_size = alg_class.key_size
            elif issubclass(alg_class, BaseHMACSigAlg):
                if key_size is not None and key_size < alg_class.min_key_size:
                    warnings.warn(
                        f"Symmetric keys to use with {alg} should be at least {alg_class.min_key_size} bits "
                        "in order to make the key at least as hard to brute-force as the signature. "
                        f"You requested a key size of {key_size} bits.",
                        stacklevel=2,
                    )
                else:
                    key_size = alg_class.min_key_size

        if key_size is None:
            warnings.warn(
                "Please provide a key_size or an alg parameter for jwskate to know the number of bits to generate. "
                "Defaulting to 128 bits.",
                stacklevel=2,
            )
            key_size = 128

        key = BinaPy.random_bits(key_size)
        return cls.from_bytes(key, alg=alg, **params)

    @classmethod
    @override
    def from_cryptography_key(cls, cryptography_key: Any, **params: Any) -> SymmetricJwk:
        return cls.from_bytes(cryptography_key, **params)

    @override
    def _to_cryptography_key(self) -> BinaPy:
        return BinaPy(self.k).decode_from("b64u")

    @override
    def thumbprint(self, hashalg: str = "SHA256") -> str:
        return BinaPy.serialize_to("json", {"k": self.k, "kty": self.kty}).to("sha256").to("b64u").ascii()

    @override
    def to_pem(self, password: bytes | str | None = None) -> str:
        msg = "Symmetric keys are not serializable to PEM."
        raise TypeError(msg)

    @property
    def key(self) -> BinaPy:
        """Returns the raw symmetric key, from the `k` parameter, base64u-decoded."""
        return self.cryptography_key  # type: ignore[no-any-return]

    @property
    def key_size(self) -> int:
        """The key size, in bits."""
        return len(self.key) * 8

    @override
    def encrypt(
        self,
        plaintext: bytes | SupportsBytes,
        *,
        aad: bytes | None = None,
        alg: str | None = None,
        iv: bytes | None = None,
    ) -> tuple[BinaPy, BinaPy, BinaPy]:
        """Encrypt arbitrary data using this key.

        Supports Authenticated Encryption with Additional Authenticated Data (use parameter `aad` for Additional
        Authenticated Data).

        An *Initialization Vector* (IV) will be generated automatically.
        You can choose your own IV by providing the `iv` parameter (only use this if you know what you are doing).

        This returns the ciphertext, the authentication tag, and the generated IV.
        If an IV was provided as parameter, the same IV is returned.

        Args:
          plaintext: the plaintext to encrypt
          aad: the Additional Authentication Data, if any
          alg: the encryption alg to use
          iv: the IV to use, if you want a specific value

        Returns:
            a (ciphertext, authentication_tag, iv) tuple

        """
        wrapper = self.encryption_wrapper(alg)
        if iv is None:
            iv = wrapper.generate_iv()

        ciphertext, tag = wrapper.encrypt(plaintext, iv=iv, aad=aad)
        return ciphertext, BinaPy(iv), tag

    @override
    def decrypt(
        self,
        ciphertext: bytes | SupportsBytes,
        *,
        iv: bytes | SupportsBytes,
        tag: bytes | SupportsBytes,
        aad: bytes | SupportsBytes | None = None,
        alg: str | None = None,
    ) -> BinaPy:
        """Decrypt arbitrary data, and verify Additional Authenticated Data.

        Args:
          ciphertext: the encrypted data
          iv: the Initialization Vector (must be the same as generated during encryption)
          tag: the authentication tag
          aad: the Additional Authenticated Data (must be the same data used during encryption)
          alg: the decryption alg (must be the same as used during encryption)

        Returns:
            the decrypted clear-text

        """
        aad = b"" if aad is None else aad
        if not isinstance(aad, bytes):
            aad = bytes(aad)
        if not isinstance(iv, bytes):
            iv = bytes(iv)
        if not isinstance(tag, bytes):
            tag = bytes(tag)

        wrapper = self.encryption_wrapper(alg)
        plaintext: bytes = wrapper.decrypt(ciphertext, auth_tag=tag, iv=iv, aad=aad)

        return BinaPy(plaintext)

    @override
    def supported_key_management_algorithms(self) -> list[str]:
        return [
            name
            for name, alg in self.KEY_MANAGEMENT_ALGORITHMS.items()
            if issubclass(alg, BaseSymmetricAlg) and alg.supports_key(self.cryptography_key)
        ]

    @override
    def supported_encryption_algorithms(self) -> list[str]:
        return [name for name, alg in self.ENCRYPTION_ALGORITHMS.items() if alg.supports_key(self.cryptography_key)]

key property

1
key: BinaPy

Returns the raw symmetric key, from the k parameter, base64u-decoded.

key_size property

1
key_size: int

The key size, in bits.

public_jwk

1
public_jwk() -> Jwk

Raise an error since Symmetric Keys are always private.

Raises:

Type Description
ValueError

symmetric keys are always private, it makes no sense to use them as public keys

Source code in jwskate/jwk/oct.py
84
85
86
87
88
89
90
91
92
93
@override
def public_jwk(self) -> Jwk:
    """Raise an error since Symmetric Keys are always private.

    Raises:
        ValueError: symmetric keys are always private, it makes no sense to use them as public keys

    """
    msg = "Symmetric keys don't have a public key"
    raise ValueError(msg)

from_bytes classmethod

1
from_bytes(k: bytes | str, **params: Any) -> SymmetricJwk

Initialize a SymmetricJwk from a raw secret key.

The provided secret key is encoded and used as the k parameter for the returned SymmetricKey.

Parameters:

Name Type Description Default
k bytes | str

the key to use

required
**params Any

additional members to include in the Jwk

{}

Returns:

Type Description
SymmetricJwk

the resulting SymmetricJwk

Source code in jwskate/jwk/oct.py
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
@classmethod
def from_bytes(cls, k: bytes | str, **params: Any) -> SymmetricJwk:
    """Initialize a `SymmetricJwk` from a raw secret key.

    The provided secret key is encoded and used as the `k` parameter for the returned `SymmetricKey`.

    Args:
      k: the key to use
      **params: additional members to include in the `Jwk`

    Returns:
      the resulting `SymmetricJwk`

    """
    return cls(dict(kty=cls.KTY, k=BinaPy(k).to("b64u").ascii(), **params))

encrypt

1
2
3
4
5
6
7
encrypt(
    plaintext: bytes | SupportsBytes,
    *,
    aad: bytes | None = None,
    alg: str | None = None,
    iv: bytes | None = None
) -> tuple[BinaPy, BinaPy, BinaPy]

Encrypt arbitrary data using this key.

Supports Authenticated Encryption with Additional Authenticated Data (use parameter aad for Additional Authenticated Data).

An Initialization Vector (IV) will be generated automatically. You can choose your own IV by providing the iv parameter (only use this if you know what you are doing).

This returns the ciphertext, the authentication tag, and the generated IV. If an IV was provided as parameter, the same IV is returned.

Parameters:

Name Type Description Default
plaintext bytes | SupportsBytes

the plaintext to encrypt

required
aad bytes | None

the Additional Authentication Data, if any

None
alg str | None

the encryption alg to use

None
iv bytes | None

the IV to use, if you want a specific value

None

Returns:

Type Description
tuple[BinaPy, BinaPy, BinaPy]

a (ciphertext, authentication_tag, iv) tuple

Source code in jwskate/jwk/oct.py
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
@override
def encrypt(
    self,
    plaintext: bytes | SupportsBytes,
    *,
    aad: bytes | None = None,
    alg: str | None = None,
    iv: bytes | None = None,
) -> tuple[BinaPy, BinaPy, BinaPy]:
    """Encrypt arbitrary data using this key.

    Supports Authenticated Encryption with Additional Authenticated Data (use parameter `aad` for Additional
    Authenticated Data).

    An *Initialization Vector* (IV) will be generated automatically.
    You can choose your own IV by providing the `iv` parameter (only use this if you know what you are doing).

    This returns the ciphertext, the authentication tag, and the generated IV.
    If an IV was provided as parameter, the same IV is returned.

    Args:
      plaintext: the plaintext to encrypt
      aad: the Additional Authentication Data, if any
      alg: the encryption alg to use
      iv: the IV to use, if you want a specific value

    Returns:
        a (ciphertext, authentication_tag, iv) tuple

    """
    wrapper = self.encryption_wrapper(alg)
    if iv is None:
        iv = wrapper.generate_iv()

    ciphertext, tag = wrapper.encrypt(plaintext, iv=iv, aad=aad)
    return ciphertext, BinaPy(iv), tag

decrypt

1
2
3
4
5
6
7
8
decrypt(
    ciphertext: bytes | SupportsBytes,
    *,
    iv: bytes | SupportsBytes,
    tag: bytes | SupportsBytes,
    aad: bytes | SupportsBytes | None = None,
    alg: str | None = None
) -> BinaPy

Decrypt arbitrary data, and verify Additional Authenticated Data.

Parameters:

Name Type Description Default
ciphertext bytes | SupportsBytes

the encrypted data

required
iv bytes | SupportsBytes

the Initialization Vector (must be the same as generated during encryption)

required
tag bytes | SupportsBytes

the authentication tag

required
aad bytes | SupportsBytes | None

the Additional Authenticated Data (must be the same data used during encryption)

None
alg str | None

the decryption alg (must be the same as used during encryption)

None

Returns:

Type Description
BinaPy

the decrypted clear-text

Source code in jwskate/jwk/oct.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
240
241
242
243
244
245
246
@override
def decrypt(
    self,
    ciphertext: bytes | SupportsBytes,
    *,
    iv: bytes | SupportsBytes,
    tag: bytes | SupportsBytes,
    aad: bytes | SupportsBytes | None = None,
    alg: str | None = None,
) -> BinaPy:
    """Decrypt arbitrary data, and verify Additional Authenticated Data.

    Args:
      ciphertext: the encrypted data
      iv: the Initialization Vector (must be the same as generated during encryption)
      tag: the authentication tag
      aad: the Additional Authenticated Data (must be the same data used during encryption)
      alg: the decryption alg (must be the same as used during encryption)

    Returns:
        the decrypted clear-text

    """
    aad = b"" if aad is None else aad
    if not isinstance(aad, bytes):
        aad = bytes(aad)
    if not isinstance(iv, bytes):
        iv = bytes(iv)
    if not isinstance(tag, bytes):
        tag = bytes(tag)

    wrapper = self.encryption_wrapper(alg)
    plaintext: bytes = wrapper.decrypt(ciphertext, auth_tag=tag, iv=iv, aad=aad)

    return BinaPy(plaintext)

OKPJwk

Bases: Jwk

Represent an Octet Key Pair keys in JWK format.

Octet Key Pair keys have Key Type "OKP".

Source code in jwskate/jwk/okp.py
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 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
132
133
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
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
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
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
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
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
class OKPJwk(Jwk):
    """Represent an Octet Key Pair keys in JWK format.

    Octet Key Pair keys have Key Type `"OKP"`.

    """

    KTY = KeyTypes.OKP

    CRYPTOGRAPHY_PRIVATE_KEY_CLASSES = (
        ed25519.Ed25519PrivateKey,
        ed448.Ed448PrivateKey,
        x25519.X25519PrivateKey,
        x448.X448PrivateKey,
    )

    CRYPTOGRAPHY_PUBLIC_KEY_CLASSES = (
        ed25519.Ed25519PublicKey,
        ed448.Ed448PublicKey,
        x25519.X25519PublicKey,
        x448.X448PublicKey,
    )

    PARAMS = {
        "crv": JwkParameter("Curve", is_private=False, is_required=True, kind="name"),
        "x": JwkParameter("Public Key", is_private=False, is_required=True, kind="b64u"),
        "d": JwkParameter("Private Key", is_private=True, is_required=True, kind="b64u"),
    }

    CURVES: Mapping[str, OKPCurve] = {curve.name: curve for curve in [Ed25519, Ed448, X448, X25519]}

    SIGNATURE_ALGORITHMS = {alg.name: alg for alg in (EdDsa,)}

    KEY_MANAGEMENT_ALGORITHMS = {
        keyalg.name: keyalg for keyalg in [EcdhEs, EcdhEs_A128KW, EcdhEs_A192KW, EcdhEs_A256KW]
    }

    @property
    @override
    def is_private(self) -> bool:
        return "d" in self

    @override
    def _validate(self) -> None:
        if not isinstance(self.crv, str) or self.crv not in self.CURVES:
            raise UnsupportedOKPCurve(self.crv)
        super()._validate()

    @classmethod
    def get_curve(cls, crv: str) -> OKPCurve:
        """Get the `OKPCurve` instance from a curve identifier.

        Args:
          crv: a curve identifier

        Returns:
            the matching `OKPCurve` instance

        Raises:
            UnsupportedOKPCurve: if the curve is not supported

        """
        curve = cls.CURVES.get(crv)
        if curve is None:
            raise UnsupportedOKPCurve(crv)
        return curve

    @property
    def curve(self) -> OKPCurve:
        """Get the `OKPCurve` instance for this key."""
        return self.get_curve(self.crv)

    @cached_property
    def public_key(self) -> bytes:
        """Get the public key from this `Jwk`, from param `x`, base64url-decoded."""
        return BinaPy(self.x).decode_from("b64u")

    @cached_property
    def private_key(self) -> bytes:
        """Get the private key from this `Jwk`, from param `d`, base64url-decoded."""
        return BinaPy(self.d).decode_from("b64u")

    @classmethod
    def from_bytes(  # noqa: C901
        cls,
        private_key: bytes,
        crv: str | None = None,
        use: str | None = None,
        **kwargs: Any,
    ) -> OKPJwk:
        """Initialize an `OKPJwk` from its private key, as `bytes`.

        The public key will be automatically derived from the supplied private key, according to the OKP curve.

        The appropriate curve will be guessed based on the key length or supplied `crv`/`use` hints:

        - 56 bytes will use `X448`
        - 57 bytes will use `Ed448`
        - 32 bytes will use `Ed25519` or `X25519`. Since there is no way to guess which one you want,
          it needs a hint with either a `crv` or `use` parameter.

        Args:
            private_key: the 32, 56 or 57 bytes private key, as raw `bytes`
            crv: the curve identifier to use
            use: the key usage
            **kwargs: additional members to include in the `Jwk`

        Returns:
            the matching `OKPJwk`

        """
        if crv and use:
            if (crv in ("Ed25519", "Ed448") and use != "sig") or (crv in ("X25519", "X448") and use != "enc"):
                msg = (
                    f"Inconsistent `crv={crv}` and `use={use}` parameters. "
                    "Ed25519 and Ed448 are used for signing (use='sig'). "
                    "X25519 and X448 are used for encryption (use='enc')."
                )
                raise ValueError(msg)
        elif crv:
            if crv in ("Ed25519", "Ed448"):
                use = "sig"
            elif crv in ("X25519", "X448"):
                use = "enc"
            else:
                raise UnsupportedOKPCurve(crv)
        elif use and use not in ("sig", "enc"):
            msg = f"Invalid `use={use}` parameter, it must be either 'sig' or 'enc'."
            raise ValueError(msg)

        cryptography_key: Any
        if len(private_key) == Ed25519.key_size:
            if use == "sig":
                cryptography_key = ed25519.Ed25519PrivateKey.from_private_bytes(private_key)
            elif use == "enc":
                cryptography_key = x25519.X25519PrivateKey.from_private_bytes(private_key)
            else:
                msg = (
                    "You provided a 32 bytes private key, which is appropriate for both Ed25519 and X25519 curves. "
                    "There is no way to guess which curve you need, so please specify either `crv='Ed25519'` "
                    "or `use='sig'` for an `Ed25519` key, or either `crv='X25519'` or `use='enc'` for a `X25519` key."
                )
                raise ValueError(msg)
        elif len(private_key) == X448.key_size:
            cryptography_key = x448.X448PrivateKey.from_private_bytes(private_key)
            if use and use != "enc":
                msg = f"Invalid `use='{use}'` parameter. Keys of length 56 bytes are only suitable for curve 'X448'."
                raise ValueError(msg)
            use = "enc"
        elif len(private_key) == Ed448.key_size:
            cryptography_key = ed448.Ed448PrivateKey.from_private_bytes(private_key)
            if use and use != "sig":
                msg = f"Invalid `use='{use}'` parameter. Keys of length 57 bytes are only suitable for curve 'Ed448'."
                raise ValueError(msg)
            use = "sig"
        else:
            msg = "Invalid private key. It must be `bytes`, of length 32, 56 or 57 bytes."
            raise ValueError(msg)

        return OKPJwk.from_cryptography_key(cryptography_key, use=use, **kwargs)

    @classmethod
    @override
    def from_cryptography_key(cls, cryptography_key: Any, **kwargs: Any) -> OKPJwk:
        for crv in OKPCurve.instances.values():
            if isinstance(
                cryptography_key,
                (crv.cryptography_private_key_class, crv.cryptography_public_key_class),
            ):
                break
        else:
            ", ".join(
                name
                for curve in OKPCurve.instances.values()
                for name in (
                    curve.cryptography_private_key_class.__name__,
                    curve.cryptography_public_key_class.__name__,
                )
            )
            msg = (
                f"Unsupported key type for OKP: {type(cryptography_key)}. "
                "Supported key types are: {supported_key_types}"
            )
            raise TypeError(msg)

        if isinstance(cryptography_key, cls.CRYPTOGRAPHY_PRIVATE_KEY_CLASSES):
            priv = cryptography_key.private_bytes(
                encoding=serialization.Encoding.Raw,
                format=serialization.PrivateFormat.Raw,
                encryption_algorithm=serialization.NoEncryption(),
            )
            pub = cryptography_key.public_key().public_bytes(
                encoding=serialization.Encoding.Raw,
                format=serialization.PublicFormat.Raw,
            )
            return cls.private(
                crv=crv.name,
                x=pub,
                d=priv,
            )
        elif isinstance(cryptography_key, cls.CRYPTOGRAPHY_PUBLIC_KEY_CLASSES):
            pub = cryptography_key.public_bytes(
                encoding=serialization.Encoding.Raw,
                format=serialization.PublicFormat.Raw,
            )
            return cls.public(
                crv=crv.name,
                x=pub,
            )
        msg = "Unsupported key type"
        raise TypeError(msg, type(cryptography_key))  # pragma: no-cover

    @override
    def _to_cryptography_key(self) -> Any:
        if self.is_private:
            return self.curve.cryptography_private_key_class.from_private_bytes(self.private_key)
        else:
            return self.curve.cryptography_public_key_class.from_public_bytes(self.public_key)

    @classmethod
    def public(cls, *, crv: str, x: bytes, **params: Any) -> OKPJwk:
        """Initialize a public `OKPJwk` based on the provided parameters.

        Args:
          crv: the key curve
          x: the public key
          **params: additional members to include in the `Jwk`

        Returns:
            the resulting `OKPJwk`

        """
        return cls(dict(kty=cls.KTY, crv=crv, x=BinaPy(x).to("b64u").ascii(), **params))

    @classmethod
    def private(cls, *, crv: str, x: bytes, d: bytes, **params: Any) -> OKPJwk:
        """Initialize a private `OKPJwk` based on the provided parameters.

        Args:
          crv: the OKP curve
          x: the public key
          d: the private key
          **params: additional members to include in the `Jwk`

        Returns:
            the resulting `OKPJwk`

        """
        return cls(
            dict(
                kty=cls.KTY,
                crv=crv,
                x=BinaPy(x).to("b64u").ascii(),
                d=BinaPy(d).to("b64u").ascii(),
                **params,
            )
        )

    @classmethod
    @override
    def generate(cls, *, crv: str | None = None, alg: str | None = None, **params: Any) -> OKPJwk:
        if crv:
            curve = cls.get_curve(crv)
        elif alg:
            if alg in cls.SIGNATURE_ALGORITHMS:
                curve = Ed25519
            elif alg in cls.KEY_MANAGEMENT_ALGORITHMS:
                curve = X25519
            else:
                raise UnsupportedAlg(alg)
        else:
            msg = (
                "You must supply at least a Curve identifier (crv) "
                "or an Algorithm identifier (alg) in order to generate an OKPJwk."
            )
            raise ValueError(msg)

        key = curve.cryptography_private_key_class.generate()
        x = key.public_key().public_bytes(serialization.Encoding.Raw, serialization.PublicFormat.Raw)
        d = key.private_bytes(
            serialization.Encoding.Raw,
            serialization.PrivateFormat.Raw,
            serialization.NoEncryption(),
        )

        return cls.private(crv=curve.name, x=x, d=d, alg=alg, **params)

    @cached_property
    @override
    def use(self) -> str | None:
        return self.curve.use

curve property

1
curve: OKPCurve

Get the OKPCurve instance for this key.

public_key cached property

1
public_key: bytes

Get the public key from this Jwk, from param x, base64url-decoded.

private_key cached property

1
private_key: bytes

Get the private key from this Jwk, from param d, base64url-decoded.

get_curve classmethod

1
get_curve(crv: str) -> OKPCurve

Get the OKPCurve instance from a curve identifier.

Parameters:

Name Type Description Default
crv str

a curve identifier

required

Returns:

Type Description
OKPCurve

the matching OKPCurve instance

Raises:

Type Description
UnsupportedOKPCurve

if the curve is not supported

Source code in jwskate/jwk/okp.py
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
@classmethod
def get_curve(cls, crv: str) -> OKPCurve:
    """Get the `OKPCurve` instance from a curve identifier.

    Args:
      crv: a curve identifier

    Returns:
        the matching `OKPCurve` instance

    Raises:
        UnsupportedOKPCurve: if the curve is not supported

    """
    curve = cls.CURVES.get(crv)
    if curve is None:
        raise UnsupportedOKPCurve(crv)
    return curve

from_bytes classmethod

1
2
3
4
5
6
from_bytes(
    private_key: bytes,
    crv: str | None = None,
    use: str | None = None,
    **kwargs: Any
) -> OKPJwk

Initialize an OKPJwk from its private key, as bytes.

The public key will be automatically derived from the supplied private key, according to the OKP curve.

The appropriate curve will be guessed based on the key length or supplied crv/use hints:

  • 56 bytes will use X448
  • 57 bytes will use Ed448
  • 32 bytes will use Ed25519 or X25519. Since there is no way to guess which one you want, it needs a hint with either a crv or use parameter.

Parameters:

Name Type Description Default
private_key bytes

the 32, 56 or 57 bytes private key, as raw bytes

required
crv str | None

the curve identifier to use

None
use str | None

the key usage

None
**kwargs Any

additional members to include in the Jwk

{}

Returns:

Type Description
OKPJwk

the matching OKPJwk

Source code in jwskate/jwk/okp.py
122
123
124
125
126
127
128
129
130
131
132
133
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
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
@classmethod
def from_bytes(  # noqa: C901
    cls,
    private_key: bytes,
    crv: str | None = None,
    use: str | None = None,
    **kwargs: Any,
) -> OKPJwk:
    """Initialize an `OKPJwk` from its private key, as `bytes`.

    The public key will be automatically derived from the supplied private key, according to the OKP curve.

    The appropriate curve will be guessed based on the key length or supplied `crv`/`use` hints:

    - 56 bytes will use `X448`
    - 57 bytes will use `Ed448`
    - 32 bytes will use `Ed25519` or `X25519`. Since there is no way to guess which one you want,
      it needs a hint with either a `crv` or `use` parameter.

    Args:
        private_key: the 32, 56 or 57 bytes private key, as raw `bytes`
        crv: the curve identifier to use
        use: the key usage
        **kwargs: additional members to include in the `Jwk`

    Returns:
        the matching `OKPJwk`

    """
    if crv and use:
        if (crv in ("Ed25519", "Ed448") and use != "sig") or (crv in ("X25519", "X448") and use != "enc"):
            msg = (
                f"Inconsistent `crv={crv}` and `use={use}` parameters. "
                "Ed25519 and Ed448 are used for signing (use='sig'). "
                "X25519 and X448 are used for encryption (use='enc')."
            )
            raise ValueError(msg)
    elif crv:
        if crv in ("Ed25519", "Ed448"):
            use = "sig"
        elif crv in ("X25519", "X448"):
            use = "enc"
        else:
            raise UnsupportedOKPCurve(crv)
    elif use and use not in ("sig", "enc"):
        msg = f"Invalid `use={use}` parameter, it must be either 'sig' or 'enc'."
        raise ValueError(msg)

    cryptography_key: Any
    if len(private_key) == Ed25519.key_size:
        if use == "sig":
            cryptography_key = ed25519.Ed25519PrivateKey.from_private_bytes(private_key)
        elif use == "enc":
            cryptography_key = x25519.X25519PrivateKey.from_private_bytes(private_key)
        else:
            msg = (
                "You provided a 32 bytes private key, which is appropriate for both Ed25519 and X25519 curves. "
                "There is no way to guess which curve you need, so please specify either `crv='Ed25519'` "
                "or `use='sig'` for an `Ed25519` key, or either `crv='X25519'` or `use='enc'` for a `X25519` key."
            )
            raise ValueError(msg)
    elif len(private_key) == X448.key_size:
        cryptography_key = x448.X448PrivateKey.from_private_bytes(private_key)
        if use and use != "enc":
            msg = f"Invalid `use='{use}'` parameter. Keys of length 56 bytes are only suitable for curve 'X448'."
            raise ValueError(msg)
        use = "enc"
    elif len(private_key) == Ed448.key_size:
        cryptography_key = ed448.Ed448PrivateKey.from_private_bytes(private_key)
        if use and use != "sig":
            msg = f"Invalid `use='{use}'` parameter. Keys of length 57 bytes are only suitable for curve 'Ed448'."
            raise ValueError(msg)
        use = "sig"
    else:
        msg = "Invalid private key. It must be `bytes`, of length 32, 56 or 57 bytes."
        raise ValueError(msg)

    return OKPJwk.from_cryptography_key(cryptography_key, use=use, **kwargs)

public classmethod

1
public(*, crv: str, x: bytes, **params: Any) -> OKPJwk

Initialize a public OKPJwk based on the provided parameters.

Parameters:

Name Type Description Default
crv str

the key curve

required
x bytes

the public key

required
**params Any

additional members to include in the Jwk

{}

Returns:

Type Description
OKPJwk

the resulting OKPJwk

Source code in jwskate/jwk/okp.py
259
260
261
262
263
264
265
266
267
268
269
270
271
272
@classmethod
def public(cls, *, crv: str, x: bytes, **params: Any) -> OKPJwk:
    """Initialize a public `OKPJwk` based on the provided parameters.

    Args:
      crv: the key curve
      x: the public key
      **params: additional members to include in the `Jwk`

    Returns:
        the resulting `OKPJwk`

    """
    return cls(dict(kty=cls.KTY, crv=crv, x=BinaPy(x).to("b64u").ascii(), **params))

private classmethod

1
2
3
private(
    *, crv: str, x: bytes, d: bytes, **params: Any
) -> OKPJwk

Initialize a private OKPJwk based on the provided parameters.

Parameters:

Name Type Description Default
crv str

the OKP curve

required
x bytes

the public key

required
d bytes

the private key

required
**params Any

additional members to include in the Jwk

{}

Returns:

Type Description
OKPJwk

the resulting OKPJwk

Source code in jwskate/jwk/okp.py
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
@classmethod
def private(cls, *, crv: str, x: bytes, d: bytes, **params: Any) -> OKPJwk:
    """Initialize a private `OKPJwk` based on the provided parameters.

    Args:
      crv: the OKP curve
      x: the public key
      d: the private key
      **params: additional members to include in the `Jwk`

    Returns:
        the resulting `OKPJwk`

    """
    return cls(
        dict(
            kty=cls.KTY,
            crv=crv,
            x=BinaPy(x).to("b64u").ascii(),
            d=BinaPy(d).to("b64u").ascii(),
            **params,
        )
    )

UnsupportedOKPCurve

Bases: KeyError

Raised when an unsupported OKP curve is requested.

Source code in jwskate/jwk/okp.py
36
37
class UnsupportedOKPCurve(KeyError):
    """Raised when an unsupported OKP curve is requested."""

RSAJwk

Bases: Jwk

Represent an RSA key in JWK format.

RSA (Rivest-Shamir-Adleman) keys have Key Type "RSA".

Source code in jwskate/jwk/rsa.py
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 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
132
133
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
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
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
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
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
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
class RSAJwk(Jwk):
    """Represent an RSA key in JWK format.

    RSA (Rivest-Shamir-Adleman) keys have Key Type `"RSA"`.

    """

    KTY = KeyTypes.RSA
    CRYPTOGRAPHY_PRIVATE_KEY_CLASSES = (rsa.RSAPrivateKey,)
    CRYPTOGRAPHY_PUBLIC_KEY_CLASSES = (rsa.RSAPublicKey,)

    PARAMS = {
        "n": JwkParameter("Modulus", is_private=False, is_required=True, kind="b64u"),
        "e": JwkParameter("Exponent", is_private=False, is_required=True, kind="b64u"),
        "d": JwkParameter("Private Exponent", is_private=True, is_required=True, kind="b64u"),
        "p": JwkParameter("First Prime Factor", is_private=True, is_required=False, kind="b64u"),
        "q": JwkParameter("Second Prime Factor", is_private=True, is_required=False, kind="b64u"),
        "dp": JwkParameter("First Factor CRT Exponent", is_private=True, is_required=False, kind="b64u"),
        "dq": JwkParameter(
            "Second Factor CRT Exponent",
            is_private=True,
            is_required=False,
            kind="b64u",
        ),
        "qi": JwkParameter("First CRT Coefficient", is_private=True, is_required=False, kind="b64u"),
        "oth": JwkParameter("Other Primes Info", is_private=True, is_required=False, kind="unsupported"),
    }

    SIGNATURE_ALGORITHMS = {sigalg.name: sigalg for sigalg in [RS256, RS384, RS512, PS256, PS384, PS512]}

    KEY_MANAGEMENT_ALGORITHMS = {
        keyalg.name: keyalg
        for keyalg in [
            RsaEsPcks1v1_5,
            RsaEsOaep,
            RsaEsOaepSha256,
            RsaEsOaepSha384,
            RsaEsOaepSha512,
        ]
    }

    @property
    @override
    def is_private(self) -> bool:
        return "d" in self

    @classmethod
    @override
    def from_cryptography_key(cls, cryptography_key: Any, **kwargs: Any) -> RSAJwk:
        if isinstance(cryptography_key, rsa.RSAPrivateKey):
            priv = cryptography_key.private_numbers()  # type: ignore[attr-defined]
            pub = cryptography_key.public_key().public_numbers()
            return cls.private(
                n=pub.n,
                e=pub.e,
                d=priv.d,
                p=priv.p,
                q=priv.q,
                dp=priv.dmp1,
                dq=priv.dmq1,
                qi=priv.iqmp,
            )
        elif isinstance(cryptography_key, rsa.RSAPublicKey):
            pub = cryptography_key.public_numbers()
            return cls.public(
                n=pub.n,
                e=pub.e,
            )
        else:
            msg = "A RSAPrivateKey or a RSAPublicKey is required."
            raise TypeError(msg)

    @override
    def _to_cryptography_key(self) -> rsa.RSAPrivateKey | rsa.RSAPublicKey:
        if self.is_private:
            return rsa.RSAPrivateNumbers(
                self.first_prime_factor,
                self.second_prime_factor,
                self.private_exponent,
                self.first_factor_crt_exponent,
                self.second_factor_crt_exponent,
                self.first_crt_coefficient,
                rsa.RSAPublicNumbers(self.exponent, self.modulus),
            ).private_key()
        else:
            return rsa.RSAPublicNumbers(e=self.exponent, n=self.modulus).public_key()

    @classmethod
    def public(cls, *, n: int, e: int = 65537, **params: Any) -> RSAJwk:
        """Initialize a public `RsaJwk` from a modulus and an exponent.

        Args:
          n: the modulus
          e: the exponent
          **params: additional parameters to include in the `Jwk`

        Returns:
          a `RSAJwk` initialized from the provided parameters

        """
        return cls(
            dict(
                kty=cls.KTY,
                n=BinaPy.from_int(n).to("b64u").ascii(),
                e=BinaPy.from_int(e).to("b64u").ascii(),
                **params,
            )
        )

    @classmethod
    def private(
        cls,
        *,
        n: int,
        e: int = 65537,
        d: int,
        p: int | None = None,
        q: int | None = None,
        dp: int | None = None,
        dq: int | None = None,
        qi: int | None = None,
        **params: Any,
    ) -> RSAJwk:
        """Initialize a private `RSAJwk` from its required parameters.

        Args:
          n: the modulus
          e: the exponent
          d: the private exponent
          p: the first prime factor
          q: the second prime factor
          dp: the first factor CRT exponent
          dq: the second factor CRT exponent
          qi: the first CRT coefficient
          **params: additional parameters to include in the `Jwk`

        Returns:
            a `RSAJwk` initialized from the given parameters

        """
        return cls(
            dict(
                kty=cls.KTY,
                n=BinaPy.from_int(n).to("b64u").ascii(),
                e=BinaPy.from_int(e).to("b64u").ascii(),
                d=BinaPy.from_int(d).to("b64u").ascii(),
                p=BinaPy.from_int(p).to("b64u").ascii() if p is not None else None,
                q=BinaPy.from_int(q).to("b64u").ascii() if q is not None else None,
                dp=BinaPy.from_int(dp).to("b64u").ascii() if dp is not None else None,
                dq=BinaPy.from_int(dq).to("b64u").ascii() if dq is not None else None,
                qi=BinaPy.from_int(qi).to("b64u").ascii() if qi is not None else None,
                **params,
            )
        )

    @classmethod
    def from_prime_factors(cls, p: int, q: int, e: int = 65537) -> RSAJwk:
        """Initialise a `RSAJwk` from its prime factors and exponent.

        Modulus and Private Exponent are mathematically calculated based on those factors.

        Exponent is usually 65537 (default).

        Args:
            p: first prime factor
            q: second prime factor
            e: exponent

        Returns:
            a `RSAJwk`

        """
        n = p * q
        phi = (p - 1) * (q - 1)
        d = pow(e, -1, phi)
        return cls.private(n=n, e=e, d=d)

    @cached_property
    def key_size(self) -> int:
        """Key size, in bits."""
        return len(BinaPy(self.n).decode_from("b64u")) * 8

    @classmethod
    def generate(cls, key_size: int = 4096, **params: Any) -> RSAJwk:
        """Generate a new random private `RSAJwk`.

        Args:
          key_size: the key size to use for the generated key, in bits
          **params: additional parameters to include in the `Jwk`

        Returns:
          a generated `RSAJwk`

        """
        private_key = rsa.generate_private_key(65537, key_size=key_size)
        pn = private_key.private_numbers()
        return cls.private(
            n=pn.public_numbers.n,
            e=pn.public_numbers.e,
            d=pn.d,
            p=pn.p,
            q=pn.q,
            dp=pn.dmp1,
            dq=pn.dmq1,
            qi=pn.iqmp,
            **params,
        )

    @cached_property
    def modulus(self) -> int:
        """Return the modulus `n` from this `Jwk`."""
        return BinaPy(self.n).decode_from("b64u").to_int()

    @cached_property
    def exponent(self) -> int:
        """Return the public exponent `e` from this `Jwk`."""
        return BinaPy(self.e).decode_from("b64u").to_int()

    @cached_property
    def private_exponent(self) -> int:
        """Return the private exponent `d` from this `Jwk`."""
        return BinaPy(self.d).decode_from("b64u").to_int()

    @cached_property
    def prime_factors(self) -> tuple[int, int]:
        """Return the 2 prime factors `p` and `q` from this `Jwk`."""
        if "p" not in self or "q" not in self:
            p, q = rsa.rsa_recover_prime_factors(self.modulus, self.exponent, self.private_exponent)
            return (p, q) if p < q else (q, p)
        return (
            BinaPy(self.p).decode_from("b64u").to_int(),
            BinaPy(self.q).decode_from("b64u").to_int(),
        )

    @cached_property
    def first_prime_factor(self) -> int:
        """Return the first prime factor `p` from this `Jwk`."""
        return self.prime_factors[0]

    @cached_property
    def second_prime_factor(self) -> int:
        """Return the second prime factor `q` from this `Jwk`."""
        return self.prime_factors[1]

    @cached_property
    def first_factor_crt_exponent(self) -> int:
        """Return the first factor CRT exponent `dp` from this `Jwk`."""
        if "dp" in self:
            return BinaPy(self.dp).decode_from("b64u").to_int()
        return rsa.rsa_crt_dmp1(self.private_exponent, self.first_prime_factor)

    @cached_property
    def second_factor_crt_exponent(self) -> int:
        """Return the second factor CRT exponent `dq` from this `Jwk`."""
        if "dq" in self:
            return BinaPy(self.dq).decode_from("b64u").to_int()
        return rsa.rsa_crt_dmq1(self.private_exponent, self.second_prime_factor)

    @cached_property
    def first_crt_coefficient(self) -> int:
        """Return the first CRT coefficient `qi` from this `Jwk`."""
        if "qi" in self:
            return BinaPy(self.qi).decode_from("b64u").to_int()
        return rsa.rsa_crt_iqmp(self.first_prime_factor, self.second_prime_factor)

    def with_optional_private_parameters(self) -> RSAJwk:
        """Compute the optional RSA private parameters.

        This returns a new `Jwk` with those additional params included.

        The optional parameters are:

        - p: first prime factor
        - q: second prime factor
        - dp: first factor Chinese Remainder Theorem exponent
        - dq: second factor Chinese Remainder Theorem exponent
        - qi: first Chinese Remainder Theorem coefficient

        """
        if not self.is_private:
            msg = "Optional private parameters can only be computed for private RSA keys."
            raise ValueError(msg)

        jwk = dict(self)

        jwk.update(
            {
                "p": BinaPy.from_int(self.first_prime_factor).to("b64u").ascii(),
                "q": BinaPy.from_int(self.second_prime_factor).to("b64u").ascii(),
                "dp": BinaPy.from_int(self.first_factor_crt_exponent).to("b64u").ascii(),
                "dq": BinaPy.from_int(self.second_factor_crt_exponent).to("b64u").ascii(),
                "qi": BinaPy.from_int(self.first_crt_coefficient).to("b64u").ascii(),
            }
        )

        return RSAJwk(jwk)

    def without_optional_private_parameters(self) -> RSAJwk:
        """Remove the optional private parameters and return another `Jwk` instance without them."""
        jwk = dict(self)
        for param in "p", "q", "dp", "dq", "qi":
            jwk.pop(param, None)

        return RSAJwk(jwk)

key_size cached property

1
key_size: int

Key size, in bits.

modulus cached property

1
modulus: int

Return the modulus n from this Jwk.

exponent cached property

1
exponent: int

Return the public exponent e from this Jwk.

private_exponent cached property

1
private_exponent: int

Return the private exponent d from this Jwk.

prime_factors cached property

1
prime_factors: tuple[int, int]

Return the 2 prime factors p and q from this Jwk.

first_prime_factor cached property

1
first_prime_factor: int

Return the first prime factor p from this Jwk.

second_prime_factor cached property

1
second_prime_factor: int

Return the second prime factor q from this Jwk.

first_factor_crt_exponent cached property

1
first_factor_crt_exponent: int

Return the first factor CRT exponent dp from this Jwk.

second_factor_crt_exponent cached property

1
second_factor_crt_exponent: int

Return the second factor CRT exponent dq from this Jwk.

first_crt_coefficient cached property

1
first_crt_coefficient: int

Return the first CRT coefficient qi from this Jwk.

public classmethod

1
public(*, n: int, e: int = 65537, **params: Any) -> RSAJwk

Initialize a public RsaJwk from a modulus and an exponent.

Parameters:

Name Type Description Default
n int

the modulus

required
e int

the exponent

65537
**params Any

additional parameters to include in the Jwk

{}

Returns:

Type Description
RSAJwk

a RSAJwk initialized from the provided parameters

Source code in jwskate/jwk/rsa.py
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
@classmethod
def public(cls, *, n: int, e: int = 65537, **params: Any) -> RSAJwk:
    """Initialize a public `RsaJwk` from a modulus and an exponent.

    Args:
      n: the modulus
      e: the exponent
      **params: additional parameters to include in the `Jwk`

    Returns:
      a `RSAJwk` initialized from the provided parameters

    """
    return cls(
        dict(
            kty=cls.KTY,
            n=BinaPy.from_int(n).to("b64u").ascii(),
            e=BinaPy.from_int(e).to("b64u").ascii(),
            **params,
        )
    )

private classmethod

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
private(
    *,
    n: int,
    e: int = 65537,
    d: int,
    p: int | None = None,
    q: int | None = None,
    dp: int | None = None,
    dq: int | None = None,
    qi: int | None = None,
    **params: Any
) -> RSAJwk

Initialize a private RSAJwk from its required parameters.

Parameters:

Name Type Description Default
n int

the modulus

required
e int

the exponent

65537
d int

the private exponent

required
p int | None

the first prime factor

None
q int | None

the second prime factor

None
dp int | None

the first factor CRT exponent

None
dq int | None

the second factor CRT exponent

None
qi int | None

the first CRT coefficient

None
**params Any

additional parameters to include in the Jwk

{}

Returns:

Type Description
RSAJwk

a RSAJwk initialized from the given parameters

Source code in jwskate/jwk/rsa.py
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
173
174
175
176
177
178
179
180
181
182
183
@classmethod
def private(
    cls,
    *,
    n: int,
    e: int = 65537,
    d: int,
    p: int | None = None,
    q: int | None = None,
    dp: int | None = None,
    dq: int | None = None,
    qi: int | None = None,
    **params: Any,
) -> RSAJwk:
    """Initialize a private `RSAJwk` from its required parameters.

    Args:
      n: the modulus
      e: the exponent
      d: the private exponent
      p: the first prime factor
      q: the second prime factor
      dp: the first factor CRT exponent
      dq: the second factor CRT exponent
      qi: the first CRT coefficient
      **params: additional parameters to include in the `Jwk`

    Returns:
        a `RSAJwk` initialized from the given parameters

    """
    return cls(
        dict(
            kty=cls.KTY,
            n=BinaPy.from_int(n).to("b64u").ascii(),
            e=BinaPy.from_int(e).to("b64u").ascii(),
            d=BinaPy.from_int(d).to("b64u").ascii(),
            p=BinaPy.from_int(p).to("b64u").ascii() if p is not None else None,
            q=BinaPy.from_int(q).to("b64u").ascii() if q is not None else None,
            dp=BinaPy.from_int(dp).to("b64u").ascii() if dp is not None else None,
            dq=BinaPy.from_int(dq).to("b64u").ascii() if dq is not None else None,
            qi=BinaPy.from_int(qi).to("b64u").ascii() if qi is not None else None,
            **params,
        )
    )

from_prime_factors classmethod

1
2
3
from_prime_factors(
    p: int, q: int, e: int = 65537
) -> RSAJwk

Initialise a RSAJwk from its prime factors and exponent.

Modulus and Private Exponent are mathematically calculated based on those factors.

Exponent is usually 65537 (default).

Parameters:

Name Type Description Default
p int

first prime factor

required
q int

second prime factor

required
e int

exponent

65537

Returns:

Type Description
RSAJwk

a RSAJwk

Source code in jwskate/jwk/rsa.py
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
@classmethod
def from_prime_factors(cls, p: int, q: int, e: int = 65537) -> RSAJwk:
    """Initialise a `RSAJwk` from its prime factors and exponent.

    Modulus and Private Exponent are mathematically calculated based on those factors.

    Exponent is usually 65537 (default).

    Args:
        p: first prime factor
        q: second prime factor
        e: exponent

    Returns:
        a `RSAJwk`

    """
    n = p * q
    phi = (p - 1) * (q - 1)
    d = pow(e, -1, phi)
    return cls.private(n=n, e=e, d=d)

generate classmethod

1
generate(key_size: int = 4096, **params: Any) -> RSAJwk

Generate a new random private RSAJwk.

Parameters:

Name Type Description Default
key_size int

the key size to use for the generated key, in bits

4096
**params Any

additional parameters to include in the Jwk

{}

Returns:

Type Description
RSAJwk

a generated RSAJwk

Source code in jwskate/jwk/rsa.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
@classmethod
def generate(cls, key_size: int = 4096, **params: Any) -> RSAJwk:
    """Generate a new random private `RSAJwk`.

    Args:
      key_size: the key size to use for the generated key, in bits
      **params: additional parameters to include in the `Jwk`

    Returns:
      a generated `RSAJwk`

    """
    private_key = rsa.generate_private_key(65537, key_size=key_size)
    pn = private_key.private_numbers()
    return cls.private(
        n=pn.public_numbers.n,
        e=pn.public_numbers.e,
        d=pn.d,
        p=pn.p,
        q=pn.q,
        dp=pn.dmp1,
        dq=pn.dmq1,
        qi=pn.iqmp,
        **params,
    )

with_optional_private_parameters

1
with_optional_private_parameters() -> RSAJwk

Compute the optional RSA private parameters.

This returns a new Jwk with those additional params included.

The optional parameters are:

  • p: first prime factor
  • q: second prime factor
  • dp: first factor Chinese Remainder Theorem exponent
  • dq: second factor Chinese Remainder Theorem exponent
  • qi: first Chinese Remainder Theorem coefficient
Source code in jwskate/jwk/rsa.py
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
def with_optional_private_parameters(self) -> RSAJwk:
    """Compute the optional RSA private parameters.

    This returns a new `Jwk` with those additional params included.

    The optional parameters are:

    - p: first prime factor
    - q: second prime factor
    - dp: first factor Chinese Remainder Theorem exponent
    - dq: second factor Chinese Remainder Theorem exponent
    - qi: first Chinese Remainder Theorem coefficient

    """
    if not self.is_private:
        msg = "Optional private parameters can only be computed for private RSA keys."
        raise ValueError(msg)

    jwk = dict(self)

    jwk.update(
        {
            "p": BinaPy.from_int(self.first_prime_factor).to("b64u").ascii(),
            "q": BinaPy.from_int(self.second_prime_factor).to("b64u").ascii(),
            "dp": BinaPy.from_int(self.first_factor_crt_exponent).to("b64u").ascii(),
            "dq": BinaPy.from_int(self.second_factor_crt_exponent).to("b64u").ascii(),
            "qi": BinaPy.from_int(self.first_crt_coefficient).to("b64u").ascii(),
        }
    )

    return RSAJwk(jwk)

without_optional_private_parameters

1
without_optional_private_parameters() -> RSAJwk

Remove the optional private parameters and return another Jwk instance without them.

Source code in jwskate/jwk/rsa.py
327
328
329
330
331
332
333
def without_optional_private_parameters(self) -> RSAJwk:
    """Remove the optional private parameters and return another `Jwk` instance without them."""
    jwk = dict(self)
    for param in "p", "q", "dp", "dq", "qi":
        jwk.pop(param, None)

    return RSAJwk(jwk)

select_alg_class

1
2
3
4
5
6
7
select_alg_class(
    supported_algs: Mapping[str, T],
    *,
    jwk_alg: str | None = None,
    alg: str | None = None,
    strict: bool = False
) -> T

Choose the appropriate alg class to use for cryptographic operations.

Given: - a mapping of supported algs names to wrapper classes - a preferred alg name (usually the one mentioned in a JWK) - and/or a user-specified alg this returns the wrapper class to use.

This checks the coherency between the user specified alg and the jwk_alg, and will emit a warning if the user specified alg is different from the jwk_alg.

Parameters:

Name Type Description Default
supported_algs Mapping[str, T]

a mapping of supported alg names to alg wrapper

required
jwk_alg str | None

the alg from the JWK, if any

None
alg str | None

a user specified alg

None
strict bool

if True and alg does not match jwk_alg, raise a MismatchingAlg exception. If False, warn instead.

False

Returns:

Type Description
T

the alg to use

Raises:

Type Description
UnsupportedAlg

if the requested alg is not supported

ValueError

if supported_algs is empty

MismatchingAlg

if alg does not match jwk_alg

Source code in jwskate/jwk/alg.py
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
def select_alg_class(
    supported_algs: Mapping[str, T],
    *,
    jwk_alg: str | None = None,
    alg: str | None = None,
    strict: bool = False,
) -> T:
    """Choose the appropriate alg class to use for cryptographic operations.

    Given:
    - a mapping of supported algs names to wrapper classes
    - a preferred alg name (usually the one mentioned in a JWK)
    - and/or a user-specified alg
    this returns the wrapper class to use.

    This checks the coherency between the user specified `alg` and the `jwk_alg`, and will emit a warning
    if the user specified alg is different from the `jwk_alg`.

    Args:
      supported_algs: a mapping of supported alg names to alg wrapper
      jwk_alg: the alg from the JWK, if any
      alg: a user specified alg
      strict: if `True` and alg does not match `jwk_alg`, raise a `MismatchingAlg` exception. If `False`, warn instead.

    Returns:
      the alg to use

    Raises:
        UnsupportedAlg: if the requested `alg` is not supported
        ValueError: if `supported_algs` is empty
        MismatchingAlg: if `alg` does not match `jwk_alg`

    """
    if not supported_algs:
        msg = "No possible algorithms to choose from!"
        raise ValueError(msg)

    choosen_alg: str
    if jwk_alg is not None:
        if alg is not None:
            if jwk_alg != alg:
                if strict:
                    raise MismatchingAlg(jwk_alg, alg)
                else:
                    warnings.warn(
                        "This key has an 'alg' parameter, you should use that alg for each operation.",
                        stacklevel=2,
                    )
            choosen_alg = alg
        else:
            choosen_alg = jwk_alg
    elif alg is not None:
        choosen_alg = alg
    else:
        msg = (
            "This key doesn't have an 'alg' parameter specifying which algorithm to use with that key, "
            "so you need to provide the expected signing alg(s) for each operation."
        )
        raise ExpectedAlgRequired(msg)

    try:
        return supported_algs[choosen_alg]
    except KeyError:
        msg = f"Alg {choosen_alg} is not supported. Supported algs: {list(supported_algs)}."
        raise UnsupportedAlg(msg) from None

select_alg_classes

1
2
3
4
5
6
7
8
select_alg_classes(
    supported_algs: Mapping[str, T],
    *,
    jwk_alg: str | None = None,
    alg: str | None = None,
    algs: Iterable[str] | None = None,
    strict: bool = False
) -> list[T]

Select several appropriate algs classes to use on cryptographic operations.

This method is typically used to get the list of valid algorithms when checking a signature, when several algorithms are allowed.

Given:

  • a mapping of supported algorithms name to wrapper classes
  • an alg parameter from a JWK
  • and/or a user-specified alg
  • and/or a user specified list of usable algs

this returns a list of supported alg wrapper classes that matches what the user specified, or, as default, the alg parameter from the JWK.

This checks the coherency between the user specified alg and the jwk_alg, and will emit a warning if the user specified alg is different from the jwk_alg.

Parameters:

Name Type Description Default
supported_algs Mapping[str, T]

a mapping of alg names to alg wrappers

required
jwk_alg str | None

the alg from the JWK, if any

None
alg str | None

a user specified alg to use, if any

None
algs Iterable[str] | None

a user specified list of algs to use, if several are allowed

None
strict bool

if True and alg does not match jwk_alg, raise a MismatchingAlg exception. If False, warn instead.

False

Returns:

Type Description
list[T]

a list of possible algs to check

Raises:

Type Description
ValueError

if both 'alg' and 'algs' parameters are used

UnsupportedAlg

if none of the requested alg are supported

Source code in jwskate/jwk/alg.py
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
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
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
def select_alg_classes(
    supported_algs: Mapping[str, T],
    *,
    jwk_alg: str | None = None,
    alg: str | None = None,
    algs: Iterable[str] | None = None,
    strict: bool = False,
) -> list[T]:
    """Select several appropriate algs classes to use on cryptographic operations.

    This method is typically used to get the list of valid algorithms when checking a signature,
    when several algorithms are allowed.

    Given:

    - a mapping of supported algorithms name to wrapper classes
    - an alg parameter from a JWK
    - and/or a user-specified alg
    - and/or a user specified list of usable algs

    this returns a list of supported alg wrapper classes that matches what the user specified, or, as default,
    the alg parameter from the JWK.

    This checks the coherency between the user specified `alg` and the `jwk_alg`, and will emit a warning
    if the user specified alg is different from the `jwk_alg`.

    Args:
      supported_algs: a mapping of alg names to alg wrappers
      jwk_alg: the alg from the JWK, if any
      alg: a user specified alg to use, if any
      algs: a user specified list of algs to use, if several are allowed
      strict: if `True` and alg does not match `jwk_alg`, raise a `MismatchingAlg` exception. If `False`, warn instead.

    Returns:
      a list of possible algs to check

    Raises:
        ValueError: if both 'alg' and 'algs' parameters are used
        UnsupportedAlg: if none of the requested alg are supported

    """
    if alg and algs:
        msg = "Please use either parameter 'alg' or 'algs', not both."
        raise ValueError(msg)

    if not supported_algs:
        msg = "No possible algorithms to choose from!"
        raise ValueError(msg)

    if jwk_alg is not None and ((alg and alg != jwk_alg) or (algs and jwk_alg not in algs)):
        if strict:
            raise MismatchingAlg(jwk_alg, alg, algs)
        else:
            requested_alg = f"{alg=}" if alg else f"{algs=}"
            warnings.warn(
                f"This key has an 'alg' parameter with value {jwk_alg}, so you should use it with that alg only."
                f"You requested {requested_alg}.",
                stacklevel=2,
            )

    possible_algs: list[str] = []
    if alg:
        possible_algs = [alg]
    elif algs:
        possible_algs = list(algs)
    elif jwk_alg:
        possible_algs = [jwk_alg]

    if possible_algs:
        possible_supported_algs = [supported_algs[alg] for alg in possible_algs if alg in supported_algs]
        if possible_supported_algs:
            return possible_supported_algs
        else:
            msg = f"None of the user-specified alg(s) are supported. {possible_algs}"
            raise UnsupportedAlg(msg)

    msg = (
        "This key doesn't have an 'alg' parameter specifying which algorithm to use with that key, "
        "so you need to provide the expected signing alg(s) for each operation."
    )
    raise ExpectedAlgRequired(msg)

to_jwk

1
2
3
4
5
6
7
to_jwk(
    key: Any,
    *,
    kty: str | None = None,
    is_private: bool | None = None,
    is_symmetric: bool | None = None
) -> Jwk

Convert any supported kind of key to a Jwk.

This optionally checks if that key is private or symmetric.

The key can be any type supported by Jwk: - a cryptography key instance - a bytes, to initialize a symmetric key - a JWK, as a dict or as a JSON formatted string - an existing Jwk instance If the supplied param is already a Jwk, it is left untouched.

Parameters:

Name Type Description Default
key Any

the key material

required
kty str | None

the expected key type

None
is_private bool | None

if True, check if the key is private, if False, check if it is public, if None, do nothing

None
is_symmetric bool | None

if True, check if the key is symmetric, if False, check if it is asymmetric, if None, do nothing

None

Returns:

Type Description
Jwk

a Jwk key

Source code in jwskate/jwk/base.py
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
def to_jwk(
    key: Any,
    *,
    kty: str | None = None,
    is_private: bool | None = None,
    is_symmetric: bool | None = None,
) -> Jwk:
    """Convert any supported kind of key to a `Jwk`.

    This optionally checks if that key is private or symmetric.

    The key can be any type supported by Jwk:
    - a `cryptography` key instance
    - a bytes, to initialize a symmetric key
    - a JWK, as a dict or as a JSON formatted string
    - an existing Jwk instance
    If the supplied param is already a Jwk, it is left untouched.

    Args:
        key: the key material
        kty: the expected key type
        is_private: if `True`, check if the key is private, if `False`, check if it is public, if `None`, do nothing
        is_symmetric: if `True`, check if the key is symmetric, if `False`, check if it is asymmetric,
            if `None`, do nothing

    Returns:
        a Jwk key

    """
    jwk = key if isinstance(key, Jwk) else Jwk(key)
    return jwk.check(kty=kty, is_private=is_private, is_symmetric=is_symmetric)

jwskate.jwt

This module contains all Json Web Key (Jwk) related classes and utilities.

InvalidJwt

Bases: ValueError

Raised when an invalid Jwt is parsed.

Source code in jwskate/jwt/base.py
18
19
class InvalidJwt(ValueError):
    """Raised when an invalid Jwt is parsed."""

Jwt

Bases: BaseCompactToken

Represents a Json Web Token.

Source code in jwskate/jwt/base.py
 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
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 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
132
133
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
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
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
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
class Jwt(BaseCompactToken):
    """Represents a Json Web Token."""

    def __new__(cls, value: bytes | str, max_size: int = 16 * 1024) -> SignedJwt | JweCompact | Jwt:  # type: ignore[misc]
        """Allow parsing both Signed and Encrypted JWTs.

        This returns the appropriate subclass or instance depending on the number of dots (.) in the serialized JWT.

        Args:
            value: the token value
            max_size: maximum allowed size for the token

        """
        if not isinstance(value, bytes):
            value = value.encode("ascii")

        if cls == Jwt:
            if value.count(b".") == 2:  # noqa: PLR2004
                from .signed import SignedJwt

                return super().__new__(SignedJwt)
            elif value.count(b".") == 4:  # noqa: PLR2004
                from jwskate.jwe import JweCompact

                return JweCompact(value, max_size)

        return super().__new__(cls)

    @classmethod
    def sign(
        cls,
        claims: Mapping[str, Any],
        key: Jwk | Mapping[str, Any] | Any,
        *,
        alg: str | None = None,
        typ: str | None = "JWT",
        extra_headers: Mapping[str, Any] | None = None,
    ) -> SignedJwt:
        """Sign a JSON payload with a private key and return the resulting `SignedJwt`.

        This method cannot generate a token without a signature. If you want to use an unsigned token (with alg=none),
        use `.unprotected()` instead.

        Args:
          claims: the payload to sign
          key: the key to use for signing
          alg: the alg to use for signing
          typ: typ (token type) header to include. If `None`, do not include this header.
          extra_headers: additional headers to include in the Jwt

        Returns:
          the resulting token

        """
        key = to_jwk(key)

        alg = alg or key.get("alg")

        if alg is None:
            msg = "a signing alg is required"
            raise ValueError(msg)

        extra_headers = extra_headers or {}
        headers = dict(alg=alg, **extra_headers)
        if typ:
            headers["typ"] = typ
        if "kid" in key:
            headers["kid"] = key.kid

        return cls.sign_arbitrary(claims=claims, headers=headers, key=key, alg=alg)

    @classmethod
    def sign_arbitrary(
        cls,
        claims: Mapping[str, Any],
        headers: Mapping[str, Any],
        key: Jwk | Mapping[str, Any] | Any,
        *,
        alg: str | None = None,
    ) -> SignedJwt:
        """Sign provided headers and claims with a private key and return the resulting `SignedJwt`.

        This does not check the consistency between headers, key, alg and kid.
        DO NOT USE THIS METHOD UNLESS YOU KNOW WHAT YOU ARE DOING!!!
        Use `Jwt.sign()` to make sure you are signing tokens properly.

        Args:
             claims: the payload to sign
             headers: the headers to sign
             key: the key to use for signing
             alg: the alg to use for signing

        """
        from .signed import SignedJwt

        key = to_jwk(key)

        alg = alg or key.get("alg")

        if alg is None:
            msg = "a signing alg is required"
            raise ValueError(msg)

        headers_part = BinaPy.serialize_to("json", headers).to("b64u")
        claims_part = BinaPy.serialize_to("json", claims).to("b64u")
        signed_value = b".".join((headers_part, claims_part))
        signature = key.sign(signed_value, alg=alg).to("b64u")
        return SignedJwt(b".".join((signed_value, signature)))

    @classmethod
    def unprotected(
        cls,
        claims: Mapping[str, Any],
        *,
        typ: str | None = "JWT",
        extra_headers: Mapping[str, Any] | None = None,
    ) -> SignedJwt:
        """Generate a JWT that is not signed and not encrypted (with alg=none).

        Args:
          claims: the claims to set in the token.
          typ: typ (token type) header to include. If `None`, do not include this header.
          extra_headers: additional headers to insert in the token.

        Returns:
            the resulting token

        """
        from .signed import SignedJwt

        headers = dict(extra_headers or {}, alg="none")
        if typ:
            headers["typ"] = typ

        headers_part = BinaPy.serialize_to("json", headers).to("b64u")
        claims_part = BinaPy.serialize_to("json", claims).to("b64u")
        signed_value = b".".join((headers_part, claims_part))
        signature = b""
        return SignedJwt(b".".join((signed_value, signature)))

    @classmethod
    def sign_and_encrypt(
        cls,
        claims: Mapping[str, Any],
        sign_key: Jwk | Mapping[str, Any] | Any,
        enc_key: Jwk | Mapping[str, Any] | Any,
        enc: str,
        *,
        sign_alg: str | None = None,
        enc_alg: str | None = None,
        sign_extra_headers: Mapping[str, Any] | None = None,
        enc_extra_headers: Mapping[str, Any] | None = None,
    ) -> JweCompact:
        """Sign a JWT, then encrypt it as JWE payload.

        This is a convenience method to do both the signing and encryption, in appropriate order.

        This is deprecated, use `Jwt.sign().encrypt()` instead.

        Args:
          claims: the payload to encrypt
          sign_key: the Jwk to use for signature
          sign_alg: the alg to use for signature
          sign_extra_headers: additional headers for the inner signed JWT
          enc_key: the Jwk to use for encryption
          enc_alg: the alg to use for CEK encryption
          enc: the alg to use for payload encryption
          enc_extra_headers: additional headers for the outer encrypted JWE

        Returns:
          the resulting JWE token, with the signed JWT as payload

        """
        return cls.sign(claims, key=sign_key, alg=sign_alg, extra_headers=sign_extra_headers).encrypt(
            enc_key, enc=enc, alg=enc_alg, extra_headers=enc_extra_headers
        )

    @classmethod
    def decrypt_nested_jwt(cls, jwe: str | JweCompact, key: Jwk | Mapping[str, Any] | Any) -> SignedJwt:
        """Decrypt a JWE that contains a nested signed JWT.

        It will return a [Jwt] instance for the inner JWT.

        Args:
            jwe: the JWE containing a nested Token
            key: the decryption key

        Returns:
            the inner JWT

        Raises:
            InvalidJwt: if the inner JWT is not valid

        """
        if not isinstance(jwe, JweCompact):
            jwe = JweCompact(jwe)
        return jwe.decrypt_jwt(key)

    @classmethod
    def decrypt_and_verify(
        cls,
        jwt: str | JweCompact,
        enc_key: Jwk | Mapping[str, Any] | Any,
        sig_key: Jwk | Mapping[str, Any] | Any,
        sig_alg: str | None = None,
        sig_algs: Iterable[str] | None = None,
    ) -> SignedJwt:
        """Decrypt then verify the signature of a JWT nested in a JWE.

        This can only be used with signed then encrypted Jwt, such as those produced by `SignedJwt.encrypt()`.

        Args:
            jwt: the JWE containing a nested signed JWT
            enc_key: the decryption key
            sig_key: the signature verification key
            sig_alg: the signature verification alg, if only 1 is allowed
            sig_algs: the signature verifications algs, if several are allowed

        Returns:
            the nested signed JWT, in clear-text, signature already verified

        Raises:
            InvalidJwt: if the JWT is not valid
            InvalidSignature: if the nested JWT signature is not valid

        """
        return cls.decrypt_nested_jwt(jwt, enc_key).verify(sig_key, alg=sig_alg, algs=sig_algs)

    @classmethod
    def timestamp(cls, delta_seconds: int = 0) -> int:
        """Return an integer timestamp that is suitable for use in JWT tokens.

        Timestamps are used in particular for `iat`, `exp` and `nbf` claims.

        A timestamp is a number of seconds since January 1st, 1970 00:00:00 UTC, ignoring leap seconds.

        By default, the current timestamp is returned. You can include `delta_seconds` to have a timestamp
        a number of seconds in the future (if positive) or in the past (if negative).

        Args:
            delta_seconds: number of seconds in the future or in the past compared to current time

        Returns:
            An integer timestamp

        """
        return int(datetime.now(timezone.utc).timestamp()) + delta_seconds

    @classmethod
    def timestamp_to_datetime(cls, timestamp: int) -> datetime:
        """Convert a JWT timestamp to a `datetime`.

        Returned datetime is always in the UTC timezone.

        Args:
            timestamp: a timestamp from a JWT token

        Returns:
            the corresponding `datetime` in UTC timezone

        """
        return datetime.fromtimestamp(timestamp, tz=timezone.utc)

__new__

1
2
3
__new__(
    value: bytes | str, max_size: int = 16 * 1024
) -> SignedJwt | JweCompact | Jwt

Allow parsing both Signed and Encrypted JWTs.

This returns the appropriate subclass or instance depending on the number of dots (.) in the serialized JWT.

Parameters:

Name Type Description Default
value bytes | str

the token value

required
max_size int

maximum allowed size for the token

16 * 1024
Source code in jwskate/jwt/base.py
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
def __new__(cls, value: bytes | str, max_size: int = 16 * 1024) -> SignedJwt | JweCompact | Jwt:  # type: ignore[misc]
    """Allow parsing both Signed and Encrypted JWTs.

    This returns the appropriate subclass or instance depending on the number of dots (.) in the serialized JWT.

    Args:
        value: the token value
        max_size: maximum allowed size for the token

    """
    if not isinstance(value, bytes):
        value = value.encode("ascii")

    if cls == Jwt:
        if value.count(b".") == 2:  # noqa: PLR2004
            from .signed import SignedJwt

            return super().__new__(SignedJwt)
        elif value.count(b".") == 4:  # noqa: PLR2004
            from jwskate.jwe import JweCompact

            return JweCompact(value, max_size)

    return super().__new__(cls)

sign classmethod

1
2
3
4
5
6
7
8
sign(
    claims: Mapping[str, Any],
    key: Jwk | Mapping[str, Any] | Any,
    *,
    alg: str | None = None,
    typ: str | None = "JWT",
    extra_headers: Mapping[str, Any] | None = None
) -> SignedJwt

Sign a JSON payload with a private key and return the resulting SignedJwt.

This method cannot generate a token without a signature. If you want to use an unsigned token (with alg=none), use .unprotected() instead.

Parameters:

Name Type Description Default
claims Mapping[str, Any]

the payload to sign

required
key Jwk | Mapping[str, Any] | Any

the key to use for signing

required
alg str | None

the alg to use for signing

None
typ str | None

typ (token type) header to include. If None, do not include this header.

'JWT'
extra_headers Mapping[str, Any] | None

additional headers to include in the Jwt

None

Returns:

Type Description
SignedJwt

the resulting token

Source code in jwskate/jwt/base.py
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
@classmethod
def sign(
    cls,
    claims: Mapping[str, Any],
    key: Jwk | Mapping[str, Any] | Any,
    *,
    alg: str | None = None,
    typ: str | None = "JWT",
    extra_headers: Mapping[str, Any] | None = None,
) -> SignedJwt:
    """Sign a JSON payload with a private key and return the resulting `SignedJwt`.

    This method cannot generate a token without a signature. If you want to use an unsigned token (with alg=none),
    use `.unprotected()` instead.

    Args:
      claims: the payload to sign
      key: the key to use for signing
      alg: the alg to use for signing
      typ: typ (token type) header to include. If `None`, do not include this header.
      extra_headers: additional headers to include in the Jwt

    Returns:
      the resulting token

    """
    key = to_jwk(key)

    alg = alg or key.get("alg")

    if alg is None:
        msg = "a signing alg is required"
        raise ValueError(msg)

    extra_headers = extra_headers or {}
    headers = dict(alg=alg, **extra_headers)
    if typ:
        headers["typ"] = typ
    if "kid" in key:
        headers["kid"] = key.kid

    return cls.sign_arbitrary(claims=claims, headers=headers, key=key, alg=alg)

sign_arbitrary classmethod

1
2
3
4
5
6
7
sign_arbitrary(
    claims: Mapping[str, Any],
    headers: Mapping[str, Any],
    key: Jwk | Mapping[str, Any] | Any,
    *,
    alg: str | None = None
) -> SignedJwt

Sign provided headers and claims with a private key and return the resulting SignedJwt.

This does not check the consistency between headers, key, alg and kid. DO NOT USE THIS METHOD UNLESS YOU KNOW WHAT YOU ARE DOING!!! Use Jwt.sign() to make sure you are signing tokens properly.

Parameters:

Name Type Description Default
claims Mapping[str, Any]

the payload to sign

required
headers Mapping[str, Any]

the headers to sign

required
key Jwk | Mapping[str, Any] | Any

the key to use for signing

required
alg str | None

the alg to use for signing

None
Source code in jwskate/jwt/base.py
 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
@classmethod
def sign_arbitrary(
    cls,
    claims: Mapping[str, Any],
    headers: Mapping[str, Any],
    key: Jwk | Mapping[str, Any] | Any,
    *,
    alg: str | None = None,
) -> SignedJwt:
    """Sign provided headers and claims with a private key and return the resulting `SignedJwt`.

    This does not check the consistency between headers, key, alg and kid.
    DO NOT USE THIS METHOD UNLESS YOU KNOW WHAT YOU ARE DOING!!!
    Use `Jwt.sign()` to make sure you are signing tokens properly.

    Args:
         claims: the payload to sign
         headers: the headers to sign
         key: the key to use for signing
         alg: the alg to use for signing

    """
    from .signed import SignedJwt

    key = to_jwk(key)

    alg = alg or key.get("alg")

    if alg is None:
        msg = "a signing alg is required"
        raise ValueError(msg)

    headers_part = BinaPy.serialize_to("json", headers).to("b64u")
    claims_part = BinaPy.serialize_to("json", claims).to("b64u")
    signed_value = b".".join((headers_part, claims_part))
    signature = key.sign(signed_value, alg=alg).to("b64u")
    return SignedJwt(b".".join((signed_value, signature)))

unprotected classmethod

1
2
3
4
5
6
unprotected(
    claims: Mapping[str, Any],
    *,
    typ: str | None = "JWT",
    extra_headers: Mapping[str, Any] | None = None
) -> SignedJwt

Generate a JWT that is not signed and not encrypted (with alg=none).

Parameters:

Name Type Description Default
claims Mapping[str, Any]

the claims to set in the token.

required
typ str | None

typ (token type) header to include. If None, do not include this header.

'JWT'
extra_headers Mapping[str, Any] | None

additional headers to insert in the token.

None

Returns:

Type Description
SignedJwt

the resulting token

Source code in jwskate/jwt/base.py
131
132
133
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
@classmethod
def unprotected(
    cls,
    claims: Mapping[str, Any],
    *,
    typ: str | None = "JWT",
    extra_headers: Mapping[str, Any] | None = None,
) -> SignedJwt:
    """Generate a JWT that is not signed and not encrypted (with alg=none).

    Args:
      claims: the claims to set in the token.
      typ: typ (token type) header to include. If `None`, do not include this header.
      extra_headers: additional headers to insert in the token.

    Returns:
        the resulting token

    """
    from .signed import SignedJwt

    headers = dict(extra_headers or {}, alg="none")
    if typ:
        headers["typ"] = typ

    headers_part = BinaPy.serialize_to("json", headers).to("b64u")
    claims_part = BinaPy.serialize_to("json", claims).to("b64u")
    signed_value = b".".join((headers_part, claims_part))
    signature = b""
    return SignedJwt(b".".join((signed_value, signature)))

sign_and_encrypt classmethod

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
sign_and_encrypt(
    claims: Mapping[str, Any],
    sign_key: Jwk | Mapping[str, Any] | Any,
    enc_key: Jwk | Mapping[str, Any] | Any,
    enc: str,
    *,
    sign_alg: str | None = None,
    enc_alg: str | None = None,
    sign_extra_headers: Mapping[str, Any] | None = None,
    enc_extra_headers: Mapping[str, Any] | None = None
) -> JweCompact

Sign a JWT, then encrypt it as JWE payload.

This is a convenience method to do both the signing and encryption, in appropriate order.

This is deprecated, use Jwt.sign().encrypt() instead.

Parameters:

Name Type Description Default
claims Mapping[str, Any]

the payload to encrypt

required
sign_key Jwk | Mapping[str, Any] | Any

the Jwk to use for signature

required
sign_alg str | None

the alg to use for signature

None
sign_extra_headers Mapping[str, Any] | None

additional headers for the inner signed JWT

None
enc_key Jwk | Mapping[str, Any] | Any

the Jwk to use for encryption

required
enc_alg str | None

the alg to use for CEK encryption

None
enc str

the alg to use for payload encryption

required
enc_extra_headers Mapping[str, Any] | None

additional headers for the outer encrypted JWE

None

Returns:

Type Description
JweCompact

the resulting JWE token, with the signed JWT as payload

Source code in jwskate/jwt/base.py
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
@classmethod
def sign_and_encrypt(
    cls,
    claims: Mapping[str, Any],
    sign_key: Jwk | Mapping[str, Any] | Any,
    enc_key: Jwk | Mapping[str, Any] | Any,
    enc: str,
    *,
    sign_alg: str | None = None,
    enc_alg: str | None = None,
    sign_extra_headers: Mapping[str, Any] | None = None,
    enc_extra_headers: Mapping[str, Any] | None = None,
) -> JweCompact:
    """Sign a JWT, then encrypt it as JWE payload.

    This is a convenience method to do both the signing and encryption, in appropriate order.

    This is deprecated, use `Jwt.sign().encrypt()` instead.

    Args:
      claims: the payload to encrypt
      sign_key: the Jwk to use for signature
      sign_alg: the alg to use for signature
      sign_extra_headers: additional headers for the inner signed JWT
      enc_key: the Jwk to use for encryption
      enc_alg: the alg to use for CEK encryption
      enc: the alg to use for payload encryption
      enc_extra_headers: additional headers for the outer encrypted JWE

    Returns:
      the resulting JWE token, with the signed JWT as payload

    """
    return cls.sign(claims, key=sign_key, alg=sign_alg, extra_headers=sign_extra_headers).encrypt(
        enc_key, enc=enc, alg=enc_alg, extra_headers=enc_extra_headers
    )

decrypt_nested_jwt classmethod

1
2
3
4
decrypt_nested_jwt(
    jwe: str | JweCompact,
    key: Jwk | Mapping[str, Any] | Any,
) -> SignedJwt

Decrypt a JWE that contains a nested signed JWT.

It will return a [Jwt] instance for the inner JWT.

Parameters:

Name Type Description Default
jwe str | JweCompact

the JWE containing a nested Token

required
key Jwk | Mapping[str, Any] | Any

the decryption key

required

Returns:

Type Description
SignedJwt

the inner JWT

Raises:

Type Description
InvalidJwt

if the inner JWT is not valid

Source code in jwskate/jwt/base.py
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
@classmethod
def decrypt_nested_jwt(cls, jwe: str | JweCompact, key: Jwk | Mapping[str, Any] | Any) -> SignedJwt:
    """Decrypt a JWE that contains a nested signed JWT.

    It will return a [Jwt] instance for the inner JWT.

    Args:
        jwe: the JWE containing a nested Token
        key: the decryption key

    Returns:
        the inner JWT

    Raises:
        InvalidJwt: if the inner JWT is not valid

    """
    if not isinstance(jwe, JweCompact):
        jwe = JweCompact(jwe)
    return jwe.decrypt_jwt(key)

decrypt_and_verify classmethod

1
2
3
4
5
6
7
decrypt_and_verify(
    jwt: str | JweCompact,
    enc_key: Jwk | Mapping[str, Any] | Any,
    sig_key: Jwk | Mapping[str, Any] | Any,
    sig_alg: str | None = None,
    sig_algs: Iterable[str] | None = None,
) -> SignedJwt

Decrypt then verify the signature of a JWT nested in a JWE.

This can only be used with signed then encrypted Jwt, such as those produced by SignedJwt.encrypt().

Parameters:

Name Type Description Default
jwt str | JweCompact

the JWE containing a nested signed JWT

required
enc_key Jwk | Mapping[str, Any] | Any

the decryption key

required
sig_key Jwk | Mapping[str, Any] | Any

the signature verification key

required
sig_alg str | None

the signature verification alg, if only 1 is allowed

None
sig_algs Iterable[str] | None

the signature verifications algs, if several are allowed

None

Returns:

Type Description
SignedJwt

the nested signed JWT, in clear-text, signature already verified

Raises:

Type Description
InvalidJwt

if the JWT is not valid

InvalidSignature

if the nested JWT signature is not valid

Source code in jwskate/jwt/base.py
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
@classmethod
def decrypt_and_verify(
    cls,
    jwt: str | JweCompact,
    enc_key: Jwk | Mapping[str, Any] | Any,
    sig_key: Jwk | Mapping[str, Any] | Any,
    sig_alg: str | None = None,
    sig_algs: Iterable[str] | None = None,
) -> SignedJwt:
    """Decrypt then verify the signature of a JWT nested in a JWE.

    This can only be used with signed then encrypted Jwt, such as those produced by `SignedJwt.encrypt()`.

    Args:
        jwt: the JWE containing a nested signed JWT
        enc_key: the decryption key
        sig_key: the signature verification key
        sig_alg: the signature verification alg, if only 1 is allowed
        sig_algs: the signature verifications algs, if several are allowed

    Returns:
        the nested signed JWT, in clear-text, signature already verified

    Raises:
        InvalidJwt: if the JWT is not valid
        InvalidSignature: if the nested JWT signature is not valid

    """
    return cls.decrypt_nested_jwt(jwt, enc_key).verify(sig_key, alg=sig_alg, algs=sig_algs)

timestamp classmethod

1
timestamp(delta_seconds: int = 0) -> int

Return an integer timestamp that is suitable for use in JWT tokens.

Timestamps are used in particular for iat, exp and nbf claims.

A timestamp is a number of seconds since January 1st, 1970 00:00:00 UTC, ignoring leap seconds.

By default, the current timestamp is returned. You can include delta_seconds to have a timestamp a number of seconds in the future (if positive) or in the past (if negative).

Parameters:

Name Type Description Default
delta_seconds int

number of seconds in the future or in the past compared to current time

0

Returns:

Type Description
int

An integer timestamp

Source code in jwskate/jwt/base.py
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
@classmethod
def timestamp(cls, delta_seconds: int = 0) -> int:
    """Return an integer timestamp that is suitable for use in JWT tokens.

    Timestamps are used in particular for `iat`, `exp` and `nbf` claims.

    A timestamp is a number of seconds since January 1st, 1970 00:00:00 UTC, ignoring leap seconds.

    By default, the current timestamp is returned. You can include `delta_seconds` to have a timestamp
    a number of seconds in the future (if positive) or in the past (if negative).

    Args:
        delta_seconds: number of seconds in the future or in the past compared to current time

    Returns:
        An integer timestamp

    """
    return int(datetime.now(timezone.utc).timestamp()) + delta_seconds

timestamp_to_datetime classmethod

1
timestamp_to_datetime(timestamp: int) -> datetime

Convert a JWT timestamp to a datetime.

Returned datetime is always in the UTC timezone.

Parameters:

Name Type Description Default
timestamp int

a timestamp from a JWT token

required

Returns:

Type Description
datetime

the corresponding datetime in UTC timezone

Source code in jwskate/jwt/base.py
270
271
272
273
274
275
276
277
278
279
280
281
282
283
@classmethod
def timestamp_to_datetime(cls, timestamp: int) -> datetime:
    """Convert a JWT timestamp to a `datetime`.

    Returned datetime is always in the UTC timezone.

    Args:
        timestamp: a timestamp from a JWT token

    Returns:
        the corresponding `datetime` in UTC timezone

    """
    return datetime.fromtimestamp(timestamp, tz=timezone.utc)

ExpiredJwt

Bases: ValueError

Raised when trying to validate an expired JWT token.

Source code in jwskate/jwt/signed.py
19
20
class ExpiredJwt(ValueError):
    """Raised when trying to validate an expired JWT token."""

InvalidClaim

Bases: ValueError

Raised when trying to validate a JWT with unexpected claims.

Source code in jwskate/jwt/signed.py
23
24
class InvalidClaim(ValueError):
    """Raised when trying to validate a JWT with unexpected claims."""

SignedJwt

Bases: Jwt

Represent a Signed Json Web Token (JWT), as defined in RFC7519.

A signed JWT contains a JSON object as payload, which represents claims.

To sign a JWT, use Jwt.sign.

Parameters:

Name Type Description Default
value bytes | str

the token value.

required
Source code in jwskate/jwt/signed.py
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 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
132
133
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
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
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
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
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
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
class SignedJwt(Jwt):
    """Represent a Signed Json Web Token (JWT), as defined in RFC7519.

    A signed JWT contains a JSON object as payload, which represents claims.

    To sign a JWT, use [Jwt.sign][jwskate.jwt.Jwt.sign].

    Args:
        value: the token value.

    """

    def __init__(self, value: bytes | str) -> None:
        super().__init__(value)

        parts = BinaPy(self.value).split(b".")
        if len(parts) != 3:  # noqa: PLR2004
            msg = "A JWT must contain a header, a payload and a signature, separated by dots"
            raise InvalidJwt(
                msg,
                value,
            )

        header, payload, signature = parts
        try:
            self.headers = header.decode_from("b64u").parse_from("json")
        except ValueError as exc:
            msg = "Invalid JWT header: it must be a Base64URL-encoded JSON object"
            raise InvalidJwt(msg) from exc

        try:
            self.claims = payload.decode_from("b64u").parse_from("json")
        except ValueError as exc:
            msg = "Invalid JWT payload: it must be a Base64URL-encoded JSON object"
            raise InvalidJwt(msg) from exc

        try:
            self.signature = signature.decode_from("b64u")
        except ValueError as exc:
            msg = "Invalid JWT signature: it must be a Base64URL-encoded binary data (bytes)"
            raise InvalidJwt(msg) from exc

    @cached_property
    def signed_part(self) -> bytes:
        """Return the actual signed data from this token.

        The signed part is composed of the header and payload, encoded in Base64-Url, joined by a dot.

        Returns:
          the signed part as bytes

        """
        return b".".join(self.value.split(b".", 2)[:2])

    def verify_signature(
        self,
        key: Jwk | Mapping[str, Any] | Any,
        alg: str | None = None,
        algs: Iterable[str] | None = None,
    ) -> bool:
        """Verify this JWT signature using a given key and algorithm(s).

        Args:
          key: the private Jwk to use to verify the signature
          alg: the alg to use to verify the signature, if only 1 is allowed
          algs: the allowed signature algs, if there are several

        Returns:
            `True` if the token signature is verified, `False` otherwise

        """
        key = to_jwk(key)

        return key.verify(data=self.signed_part, signature=self.signature, alg=alg, algs=algs)

    def verify(self, key: Jwk | Any, *, alg: str | None = None, algs: Iterable[str] | None = None) -> Self:
        """Convenience method to verify the signature inline.

        Returns `self` on success, raises an exception on failure.

        Raises:
            InvalidSignature: if the signature does not verify.

        Return:
            the same `SignedJwt`, if the signature is verified.

        Usage:
            ```python
            jwt = SignedJwt(
                "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJURVNUIn0.tIUFZqEZD12odEyBWscuxc4USspdYfJKhxPN0JXVMK97SUM69HrU5MGgocyyBbx1x9yIAkV7rNjcviqwGoVvsQ"
            ).verify(
                {
                    "kty": "EC",
                    "alg": "ES256",
                    "crv": "P-256",
                    "x": "T_RLrReYRPIknDpIEjLUoy7ibAbqJDfHe03mkEjI_oU",
                    "y": "8MM4v58j8IHag6uibgC0Qn275bl9c9JR0UD0TwFgMPM",
                }
            )

            # you can now do your business with this verified JWT:
            assert jwt.claims == {"sub": "TEST"}
            ```

        """
        if self.verify_signature(key, alg=alg, algs=algs):
            return self
        raise InvalidSignature(data=self, key=key, alg=alg, algs=algs)

    def is_expired(self, leeway: int = 0) -> bool | None:
        """Check if this token is expired, based on its `exp` claim.

        Args:
            leeway: additional number of seconds for leeway.

        Returns:
            `True` if the token is expired, `False` if it's not, `None` if there is no `exp` claim.

        """
        exp = self.expires_at
        if exp is None:
            return None
        return exp < (datetime.now(timezone.utc) + timedelta(seconds=leeway))

    @cached_property
    def expires_at(self) -> datetime | None:
        """Get the *Expires At* (`exp`) date from this token.

        Returns:
          a `datetime` initialized from the `exp` claim, or `None` if there is no `exp` claim

        Raises:
            AttributeError: if the `exp` claim cannot be parsed to a date

        """
        exp = self.get_claim("exp")
        if not exp:
            return None
        try:
            exp_dt = Jwt.timestamp_to_datetime(exp)
        except (TypeError, OSError):
            msg = "invalid `exp `claim"
            raise AttributeError(msg, exp) from None
        else:
            return exp_dt

    @cached_property
    def issued_at(self) -> datetime | None:
        """Get the *Issued At* (`iat`) date from this token.

        Returns:
          a `datetime` initialized from the `iat` claim, or `None` if there is no `iat` claim

        Raises:
            AttributeError: if the `iss` claim cannot be parsed to a date

        """
        iat = self.get_claim("iat")
        if not iat:
            return None
        try:
            iat_dt = Jwt.timestamp_to_datetime(iat)
        except (TypeError, OSError):
            msg = "invalid `iat `claim"
            raise AttributeError(msg, iat) from None
        else:
            return iat_dt

    @cached_property
    def not_before(self) -> datetime | None:
        """Get the *Not Before* (nbf) date from this token.

        Returns:
          a `datetime` initialized from the `nbf` claim, or `None` if there is no `nbf` claim

        Raises:
            AttributeError: if the `nbf` claim cannot be parsed to a date

        """
        nbf = self.get_claim("nbf")
        if not nbf:
            return None
        try:
            nbf_dt = Jwt.timestamp_to_datetime(nbf)
        except (TypeError, OSError):
            msg = "invalid `nbf `claim"
            raise AttributeError(msg, nbf) from None
        else:
            return nbf_dt

    @cached_property
    def issuer(self) -> str | None:
        """Get the *Issuer* (`iss`) claim from this token.

        Returns:
          the issuer, as `str`, or `None` if there is no `ìss` claim

        Raises:
            AttributeError: if the `ìss` claim value is not a string

        """
        iss = self.get_claim("iss")
        if iss is None or isinstance(iss, str):
            return iss
        msg = "iss has an unexpected type"
        raise AttributeError(msg, type(iss))

    @cached_property
    def audiences(self) -> list[str]:
        """Get the *Audience(s)* (`aud`) claim from this token.

        If this token has a single audience, this will return a `list` anyway.

        Returns:
            the list of audiences from this token, from the `aud` claim.

        Raises:
            AttributeError: if the audience is an unexpected type

        """
        aud = self.get_claim("aud")
        if aud is None:
            return []
        if isinstance(aud, str):
            return [aud]
        if isinstance(aud, list):
            return aud
        msg = "aud has an unexpected type"
        raise AttributeError(msg, type(aud))

    @cached_property
    def subject(self) -> str | None:
        """Get the *Subject* (`sub`) from this token.

        Returns:
          the subject, as `str`, or `None` if there is no `sub` claim

        Raises:
            AttributeError: if the `sub` value is not a string

        """
        sub = self.get_claim("sub")
        if sub is None or isinstance(sub, str):
            return sub
        msg = "sub has an unexpected type"
        raise AttributeError(msg, type(sub))

    @cached_property
    def jwt_token_id(self) -> str | None:
        """Get the *JWT Token ID* (`jti`) from this token.

        Returns:
          the token identifier, as `str`, or `None` if there is no `jti` claim

        Raises:
          AttributeError: if the `jti` value is not a string

        """
        jti = self.get_claim("jti")
        if jti is None or isinstance(jti, str):
            return jti
        msg = "jti has an unexpected type"
        raise AttributeError(msg, type(jti))

    def get_claim(self, key: str, default: Any = None) -> Any:
        """Get a claim by name from this Jwt.

        Args:
          key: the claim name.
          default: a default value if the claim is not found

        Returns:
          the claim value if found, or `default` if not found

        """
        return self.claims.get(key, default)

    def __getitem__(self, item: str) -> Any:
        """Allow access to claim by name with subscription.

        Args:
          item: the claim name

        Returns:
         the claim value

        """
        value = self.get_claim(item)
        if value is None:
            raise KeyError(item)
        return value

    def __getattr__(self, item: str) -> Any:
        """Allow claim access as attributes.

        Args:
            item: the claim name

        Returns:
            the claim value

        """
        value = self.get_claim(item)
        if value is None:
            raise AttributeError(item)
        return value

    def __str__(self) -> str:
        """Return the Jwt serialized value, as `str`.

        Returns:
            the serialized token value.

        """
        return self.value.decode()

    def __bytes__(self) -> bytes:
        """Return the Jwt serialized value, as `bytes`.

        Returns:
            the serialized token value.

        """
        return self.value

    def validate(
        self,
        key: Jwk | Mapping[str, Any] | Any,
        *,
        alg: str | None = None,
        algs: Iterable[str] | None = None,
        issuer: str | None = None,
        audience: None | str = None,
        check_exp: bool = True,
        **kwargs: Any,
    ) -> None:
        """Validate a `SignedJwt` signature and expected claims.

        This verifies the signature using the provided `jwk` and `alg`, then checks the token issuer, audience and
        expiration date.
        This can also check custom claims using extra `kwargs`, whose values can be:

        - a static value (`str`, `int`, etc.): the value from the token will be compared "as-is".
        - a callable, taking the claim value as parameter: if that callable returns `True`, the claim is considered
        as valid.

        Args:
          key: the signing key to use to verify the signature.
          alg: the signature alg to use to verify the signature.
          algs: allowed signature algs, if several
          issuer: the expected issuer for this token.
          audience: the expected audience for this token.
          check_exp: ìf `True` (default), check that the token is not expired.
          **kwargs: additional claims to check

        Returns:
          Raises exceptions if any validation check fails.

        Raises:
          InvalidSignature: if the signature is not valid
          InvalidClaim: if a claim doesn't validate
          ExpiredJwt: if the expiration date is passed

        """
        self.verify(key, alg=alg, algs=algs)

        if issuer is not None and self.issuer != issuer:
            msg = "Unexpected issuer"
            raise InvalidClaim(msg, "iss", self.issuer)

        if audience is not None and (self.audiences is None or audience not in self.audiences):
            msg = "Unexpected audience"
            raise InvalidClaim(msg, "aud", self.audiences)

        if check_exp:
            expired = self.is_expired()
            if expired is True:
                msg = f"This token expired at {self.expires_at}"
                raise ExpiredJwt(msg)
            elif expired is None:
                msg = "This token does not contain an 'exp' claim."
                raise InvalidClaim(msg, "exp")

        for key, value in kwargs.items():
            claim = self.get_claim(key)
            if callable(value):
                if not value(claim):
                    raise InvalidClaim(
                        key,
                        f"value of claim {key} doesn't validate with the provided validator",
                        claim,
                    )
            elif claim != value:
                raise InvalidClaim(key, f"unexpected value for claim {key}", claim)

    def encrypt(
        self, key: Any, enc: str, alg: str | None = None, extra_headers: Mapping[str, Any] | None = None
    ) -> JweCompact:
        """Encrypt this JWT into a JWE.

        The result is an encrypted (outer) JWT containing a signed (inner) JWT.

        Arguments:
            key: the encryption key to use
            enc: the encryption alg to use
            alg: the key management alg to use
            extra_headers: additional headers to include in the outer JWE.

        """
        extra_headers = dict(extra_headers) if extra_headers else {}
        extra_headers.setdefault("cty", "JWT")

        jwe = JweCompact.encrypt(self, key, enc=enc, alg=alg, extra_headers=extra_headers)
        return jwe

signed_part cached property

1
signed_part: bytes

Return the actual signed data from this token.

The signed part is composed of the header and payload, encoded in Base64-Url, joined by a dot.

Returns:

Type Description
bytes

the signed part as bytes

expires_at cached property

1
expires_at: datetime | None

Get the Expires At (exp) date from this token.

Returns:

Type Description
datetime | None

a datetime initialized from the exp claim, or None if there is no exp claim

Raises:

Type Description
AttributeError

if the exp claim cannot be parsed to a date

issued_at cached property

1
issued_at: datetime | None

Get the Issued At (iat) date from this token.

Returns:

Type Description
datetime | None

a datetime initialized from the iat claim, or None if there is no iat claim

Raises:

Type Description
AttributeError

if the iss claim cannot be parsed to a date

not_before cached property

1
not_before: datetime | None

Get the Not Before (nbf) date from this token.

Returns:

Type Description
datetime | None

a datetime initialized from the nbf claim, or None if there is no nbf claim

Raises:

Type Description
AttributeError

if the nbf claim cannot be parsed to a date

issuer cached property

1
issuer: str | None

Get the Issuer (iss) claim from this token.

Returns:

Type Description
str | None

the issuer, as str, or None if there is no ìss claim

Raises:

Type Description
AttributeError

if the ìss claim value is not a string

audiences cached property

1
audiences: list[str]

Get the Audience(s) (aud) claim from this token.

If this token has a single audience, this will return a list anyway.

Returns:

Type Description
list[str]

the list of audiences from this token, from the aud claim.

Raises:

Type Description
AttributeError

if the audience is an unexpected type

subject cached property

1
subject: str | None

Get the Subject (sub) from this token.

Returns:

Type Description
str | None

the subject, as str, or None if there is no sub claim

Raises:

Type Description
AttributeError

if the sub value is not a string

jwt_token_id cached property

1
jwt_token_id: str | None

Get the JWT Token ID (jti) from this token.

Returns:

Type Description
str | None

the token identifier, as str, or None if there is no jti claim

Raises:

Type Description
AttributeError

if the jti value is not a string

verify_signature

1
2
3
4
5
verify_signature(
    key: Jwk | Mapping[str, Any] | Any,
    alg: str | None = None,
    algs: Iterable[str] | None = None,
) -> bool

Verify this JWT signature using a given key and algorithm(s).

Parameters:

Name Type Description Default
key Jwk | Mapping[str, Any] | Any

the private Jwk to use to verify the signature

required
alg str | None

the alg to use to verify the signature, if only 1 is allowed

None
algs Iterable[str] | None

the allowed signature algs, if there are several

None

Returns:

Type Description
bool

True if the token signature is verified, False otherwise

Source code in jwskate/jwt/signed.py
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
def verify_signature(
    self,
    key: Jwk | Mapping[str, Any] | Any,
    alg: str | None = None,
    algs: Iterable[str] | None = None,
) -> bool:
    """Verify this JWT signature using a given key and algorithm(s).

    Args:
      key: the private Jwk to use to verify the signature
      alg: the alg to use to verify the signature, if only 1 is allowed
      algs: the allowed signature algs, if there are several

    Returns:
        `True` if the token signature is verified, `False` otherwise

    """
    key = to_jwk(key)

    return key.verify(data=self.signed_part, signature=self.signature, alg=alg, algs=algs)

verify

1
2
3
4
5
6
verify(
    key: Jwk | Any,
    *,
    alg: str | None = None,
    algs: Iterable[str] | None = None
) -> Self

Convenience method to verify the signature inline.

Returns self on success, raises an exception on failure.

Raises:

Type Description
InvalidSignature

if the signature does not verify.

Return

the same SignedJwt, if the signature is verified.

Usage
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
jwt = SignedJwt(
    "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJURVNUIn0.tIUFZqEZD12odEyBWscuxc4USspdYfJKhxPN0JXVMK97SUM69HrU5MGgocyyBbx1x9yIAkV7rNjcviqwGoVvsQ"
).verify(
    {
        "kty": "EC",
        "alg": "ES256",
        "crv": "P-256",
        "x": "T_RLrReYRPIknDpIEjLUoy7ibAbqJDfHe03mkEjI_oU",
        "y": "8MM4v58j8IHag6uibgC0Qn275bl9c9JR0UD0TwFgMPM",
    }
)

# you can now do your business with this verified JWT:
assert jwt.claims == {"sub": "TEST"}
Source code in jwskate/jwt/signed.py
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
132
133
134
def verify(self, key: Jwk | Any, *, alg: str | None = None, algs: Iterable[str] | None = None) -> Self:
    """Convenience method to verify the signature inline.

    Returns `self` on success, raises an exception on failure.

    Raises:
        InvalidSignature: if the signature does not verify.

    Return:
        the same `SignedJwt`, if the signature is verified.

    Usage:
        ```python
        jwt = SignedJwt(
            "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJURVNUIn0.tIUFZqEZD12odEyBWscuxc4USspdYfJKhxPN0JXVMK97SUM69HrU5MGgocyyBbx1x9yIAkV7rNjcviqwGoVvsQ"
        ).verify(
            {
                "kty": "EC",
                "alg": "ES256",
                "crv": "P-256",
                "x": "T_RLrReYRPIknDpIEjLUoy7ibAbqJDfHe03mkEjI_oU",
                "y": "8MM4v58j8IHag6uibgC0Qn275bl9c9JR0UD0TwFgMPM",
            }
        )

        # you can now do your business with this verified JWT:
        assert jwt.claims == {"sub": "TEST"}
        ```

    """
    if self.verify_signature(key, alg=alg, algs=algs):
        return self
    raise InvalidSignature(data=self, key=key, alg=alg, algs=algs)

is_expired

1
is_expired(leeway: int = 0) -> bool | None

Check if this token is expired, based on its exp claim.

Parameters:

Name Type Description Default
leeway int

additional number of seconds for leeway.

0

Returns:

Type Description
bool | None

True if the token is expired, False if it's not, None if there is no exp claim.

Source code in jwskate/jwt/signed.py
136
137
138
139
140
141
142
143
144
145
146
147
148
149
def is_expired(self, leeway: int = 0) -> bool | None:
    """Check if this token is expired, based on its `exp` claim.

    Args:
        leeway: additional number of seconds for leeway.

    Returns:
        `True` if the token is expired, `False` if it's not, `None` if there is no `exp` claim.

    """
    exp = self.expires_at
    if exp is None:
        return None
    return exp < (datetime.now(timezone.utc) + timedelta(seconds=leeway))

get_claim

1
get_claim(key: str, default: Any = None) -> Any

Get a claim by name from this Jwt.

Parameters:

Name Type Description Default
key str

the claim name.

required
default Any

a default value if the claim is not found

None

Returns:

Type Description
Any

the claim value if found, or default if not found

Source code in jwskate/jwt/signed.py
291
292
293
294
295
296
297
298
299
300
301
302
def get_claim(self, key: str, default: Any = None) -> Any:
    """Get a claim by name from this Jwt.

    Args:
      key: the claim name.
      default: a default value if the claim is not found

    Returns:
      the claim value if found, or `default` if not found

    """
    return self.claims.get(key, default)

__getitem__

1
__getitem__(item: str) -> Any

Allow access to claim by name with subscription.

Parameters:

Name Type Description Default
item str

the claim name

required

Returns:

Type Description
Any

the claim value

Source code in jwskate/jwt/signed.py
304
305
306
307
308
309
310
311
312
313
314
315
316
317
def __getitem__(self, item: str) -> Any:
    """Allow access to claim by name with subscription.

    Args:
      item: the claim name

    Returns:
     the claim value

    """
    value = self.get_claim(item)
    if value is None:
        raise KeyError(item)
    return value

__getattr__

1
__getattr__(item: str) -> Any

Allow claim access as attributes.

Parameters:

Name Type Description Default
item str

the claim name

required

Returns:

Type Description
Any

the claim value

Source code in jwskate/jwt/signed.py
319
320
321
322
323
324
325
326
327
328
329
330
331
332
def __getattr__(self, item: str) -> Any:
    """Allow claim access as attributes.

    Args:
        item: the claim name

    Returns:
        the claim value

    """
    value = self.get_claim(item)
    if value is None:
        raise AttributeError(item)
    return value

__str__

1
__str__() -> str

Return the Jwt serialized value, as str.

Returns:

Type Description
str

the serialized token value.

Source code in jwskate/jwt/signed.py
334
335
336
337
338
339
340
341
def __str__(self) -> str:
    """Return the Jwt serialized value, as `str`.

    Returns:
        the serialized token value.

    """
    return self.value.decode()

__bytes__

1
__bytes__() -> bytes

Return the Jwt serialized value, as bytes.

Returns:

Type Description
bytes

the serialized token value.

Source code in jwskate/jwt/signed.py
343
344
345
346
347
348
349
350
def __bytes__(self) -> bytes:
    """Return the Jwt serialized value, as `bytes`.

    Returns:
        the serialized token value.

    """
    return self.value

validate

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
validate(
    key: Jwk | Mapping[str, Any] | Any,
    *,
    alg: str | None = None,
    algs: Iterable[str] | None = None,
    issuer: str | None = None,
    audience: None | str = None,
    check_exp: bool = True,
    **kwargs: Any
) -> None

Validate a SignedJwt signature and expected claims.

This verifies the signature using the provided jwk and alg, then checks the token issuer, audience and expiration date. This can also check custom claims using extra kwargs, whose values can be:

  • a static value (str, int, etc.): the value from the token will be compared "as-is".
  • a callable, taking the claim value as parameter: if that callable returns True, the claim is considered as valid.

Parameters:

Name Type Description Default
key Jwk | Mapping[str, Any] | Any

the signing key to use to verify the signature.

required
alg str | None

the signature alg to use to verify the signature.

None
algs Iterable[str] | None

allowed signature algs, if several

None
issuer str | None

the expected issuer for this token.

None
audience None | str

the expected audience for this token.

None
check_exp bool

ìf True (default), check that the token is not expired.

True
**kwargs Any

additional claims to check

{}

Returns:

Type Description
None

Raises exceptions if any validation check fails.

Raises:

Type Description
InvalidSignature

if the signature is not valid

InvalidClaim

if a claim doesn't validate

ExpiredJwt

if the expiration date is passed

Source code in jwskate/jwt/signed.py
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
def validate(
    self,
    key: Jwk | Mapping[str, Any] | Any,
    *,
    alg: str | None = None,
    algs: Iterable[str] | None = None,
    issuer: str | None = None,
    audience: None | str = None,
    check_exp: bool = True,
    **kwargs: Any,
) -> None:
    """Validate a `SignedJwt` signature and expected claims.

    This verifies the signature using the provided `jwk` and `alg`, then checks the token issuer, audience and
    expiration date.
    This can also check custom claims using extra `kwargs`, whose values can be:

    - a static value (`str`, `int`, etc.): the value from the token will be compared "as-is".
    - a callable, taking the claim value as parameter: if that callable returns `True`, the claim is considered
    as valid.

    Args:
      key: the signing key to use to verify the signature.
      alg: the signature alg to use to verify the signature.
      algs: allowed signature algs, if several
      issuer: the expected issuer for this token.
      audience: the expected audience for this token.
      check_exp: ìf `True` (default), check that the token is not expired.
      **kwargs: additional claims to check

    Returns:
      Raises exceptions if any validation check fails.

    Raises:
      InvalidSignature: if the signature is not valid
      InvalidClaim: if a claim doesn't validate
      ExpiredJwt: if the expiration date is passed

    """
    self.verify(key, alg=alg, algs=algs)

    if issuer is not None and self.issuer != issuer:
        msg = "Unexpected issuer"
        raise InvalidClaim(msg, "iss", self.issuer)

    if audience is not None and (self.audiences is None or audience not in self.audiences):
        msg = "Unexpected audience"
        raise InvalidClaim(msg, "aud", self.audiences)

    if check_exp:
        expired = self.is_expired()
        if expired is True:
            msg = f"This token expired at {self.expires_at}"
            raise ExpiredJwt(msg)
        elif expired is None:
            msg = "This token does not contain an 'exp' claim."
            raise InvalidClaim(msg, "exp")

    for key, value in kwargs.items():
        claim = self.get_claim(key)
        if callable(value):
            if not value(claim):
                raise InvalidClaim(
                    key,
                    f"value of claim {key} doesn't validate with the provided validator",
                    claim,
                )
        elif claim != value:
            raise InvalidClaim(key, f"unexpected value for claim {key}", claim)

encrypt

1
2
3
4
5
6
encrypt(
    key: Any,
    enc: str,
    alg: str | None = None,
    extra_headers: Mapping[str, Any] | None = None,
) -> JweCompact

Encrypt this JWT into a JWE.

The result is an encrypted (outer) JWT containing a signed (inner) JWT.

Parameters:

Name Type Description Default
key Any

the encryption key to use

required
enc str

the encryption alg to use

required
alg str | None

the key management alg to use

None
extra_headers Mapping[str, Any] | None

additional headers to include in the outer JWE.

None
Source code in jwskate/jwt/signed.py
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
def encrypt(
    self, key: Any, enc: str, alg: str | None = None, extra_headers: Mapping[str, Any] | None = None
) -> JweCompact:
    """Encrypt this JWT into a JWE.

    The result is an encrypted (outer) JWT containing a signed (inner) JWT.

    Arguments:
        key: the encryption key to use
        enc: the encryption alg to use
        alg: the key management alg to use
        extra_headers: additional headers to include in the outer JWE.

    """
    extra_headers = dict(extra_headers) if extra_headers else {}
    extra_headers.setdefault("cty", "JWT")

    jwe = JweCompact.encrypt(self, key, enc=enc, alg=alg, extra_headers=extra_headers)
    return jwe

JwtSigner

A helper class to easily sign JWTs with standardized claims.

The standardized claims include:

  • ÃŒat: issued at date
  • exp: expiration date
  • nbf: not before date
  • iss: issuer identifier
  • sub: subject identifier
  • aud: audience identifier
  • jti: JWT token ID

The issuer, signing keys, signing alg and default lifetime are defined at initialization time, so you only have to define the subject, audience and custom claims when calling JwtSigner.sign(). This can be used as an alternative to Jwt.sign() when a single issuer issues multiple tokens.

Parameters:

Name Type Description Default
issuer str | None

the issuer string to use as ìss claim for signed tokens.

None
key Jwk | Any

the private Jwk to use to sign tokens.

required
alg str | None

the signing alg to use to sign tokens.

None
default_lifetime int

the default lifetime, in seconds, to use for claim exp. This can be overridden when calling .sign()

60
default_leeway int | None

the default leeway, in seconds, to use for claim nbf. If None, no nbf claim is included. This can be overridden when calling .sign()

None
Source code in jwskate/jwt/signer.py
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 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
132
133
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
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
class JwtSigner:
    """A helper class to easily sign JWTs with standardized claims.

    The standardized claims include:

       - `ÃŒat`: issued at date
       - `exp`: expiration date
       - `nbf`: not before date
       - `iss`: issuer identifier
       - `sub`: subject identifier
       - `aud`: audience identifier
       - `jti`: JWT token ID

    The issuer, signing keys, signing alg and default lifetime are
    defined at initialization time, so you only have to define the
    subject, audience and custom claims when calling `JwtSigner.sign()`.
    This can be used as an alternative to `Jwt.sign()` when a single
    issuer issues multiple tokens.

    Args:
        issuer: the issuer string to use as `ìss` claim for signed tokens.
        key: the private Jwk to use to sign tokens.
        alg: the signing alg to use to sign tokens.
        default_lifetime: the default lifetime, in seconds, to use for claim `exp`. This can be overridden
            when calling `.sign()`
        default_leeway: the default leeway, in seconds, to use for claim `nbf`. If None, no `nbf` claim is
            included. This can be overridden when calling `.sign()`

    """

    def __init__(
        self,
        key: Jwk | Any,
        *,
        issuer: str | None = None,
        alg: str | None = None,
        default_lifetime: int = 60,
        default_leeway: int | None = None,
    ):
        self.issuer = issuer
        self.jwk = to_jwk(key)
        self.alg = alg
        self.default_lifetime = default_lifetime
        self.default_leeway = default_leeway

    def sign(
        self,
        *,
        subject: str | None = None,
        audience: str | Iterable[str] | None = None,
        extra_claims: Mapping[str, Any] | None = None,
        extra_headers: Mapping[str, Any] | None = None,
        lifetime: int | None = None,
        leeway: int | None = None,
    ) -> SignedJwt:
        """Sign a Jwt.

        Claim 'issuer' will have the value defined at initialization time. Claim `iat`, `nbf` and `exp` will reflect
        the current time when the token is signed. `exp` includes `lifetime` seconds in the future, and `nbf`
        includes `leeway` seconds in the past.

        Args:
          subject: the subject to include in claim `sub`. (Default value = None)
          audience: the audience identifier(s) to include in claim `aud`.
          extra_claims: additional claims to include in the signed token. (Default value = None)
          extra_headers: additional headers to include in the header part. (Default value = None)
          lifetime: lifetime, in seconds, to use for the `exp` claim. If None, use the default_lifetime defined at
            initialization time.
          leeway: leeway, in seconds, to use for the `nbf` claim. If None, use the default_leeway defined at
            initialization time.

        Returns:
          the resulting signed token.

        """
        now = Jwt.timestamp()
        lifetime = lifetime or self.default_lifetime
        exp = now + lifetime
        leeway = leeway or self.default_leeway
        nbf = (now - leeway) if leeway is not None else None
        jti = self.generate_jti()
        extra_claims = extra_claims or {}
        claims = {
            key: value
            for key, value in dict(
                extra_claims,
                iss=self.issuer,
                aud=audience,
                sub=subject,
                iat=now,
                exp=exp,
                nbf=nbf,
                jti=jti,
            ).items()
            if value is not None
        }
        return Jwt.sign(claims, key=self.jwk, alg=self.alg, extra_headers=extra_headers)

    def generate_jti(self) -> str:
        """Generate Jwt Token ID (jti) values.

        Default uses UUID4. Can be overridden in subclasses.

        Returns:
            A unique value suitable for use as JWT Token ID (jti) claim.

        """
        return str(uuid.uuid4())

    @classmethod
    def with_random_key(
        cls,
        *,
        issuer: str,
        alg: str,
        default_lifetime: int = 60,
        default_leeway: int | None = None,
        kid: str | None = None,
    ) -> JwtSigner:
        """Initialize a JwtSigner with a randomly generated key.

        Args:
            issuer: the issuer identifier
            alg: the signing alg to use
            default_lifetime: lifetime for generated tokens expiration date (`exp` claim)
            default_leeway: leeway for generated tokens not before date (`nbf` claim)
            kid: key id to use for the generated key

        Returns:
            a JwtSigner initialized with a random key

        """
        jwk = Jwk.generate_for_alg(alg, kid=kid).with_kid_thumbprint()
        return cls(issuer=issuer, key=jwk, alg=alg, default_lifetime=default_lifetime, default_leeway=default_leeway)

    def verifier(
        self,
        *,
        audience: str,
        verifiers: Iterable[Callable[[SignedJwt], None]] | None = None,
        **kwargs: Any,
    ) -> JwtVerifier:
        """Return the matching `JwtVerifier`, initialized with the public key."""
        return JwtVerifier(
            issuer=self.issuer,
            jwkset=self.jwk.public_jwk().as_jwks(),
            alg=self.alg,
            audience=audience,
            verifiers=verifiers,
            **kwargs,
        )

sign

1
2
3
4
5
6
7
8
9
sign(
    *,
    subject: str | None = None,
    audience: str | Iterable[str] | None = None,
    extra_claims: Mapping[str, Any] | None = None,
    extra_headers: Mapping[str, Any] | None = None,
    lifetime: int | None = None,
    leeway: int | None = None
) -> SignedJwt

Sign a Jwt.

Claim 'issuer' will have the value defined at initialization time. Claim iat, nbf and exp will reflect the current time when the token is signed. exp includes lifetime seconds in the future, and nbf includes leeway seconds in the past.

Parameters:

Name Type Description Default
subject str | None

the subject to include in claim sub. (Default value = None)

None
audience str | Iterable[str] | None

the audience identifier(s) to include in claim aud.

None
extra_claims Mapping[str, Any] | None

additional claims to include in the signed token. (Default value = None)

None
extra_headers Mapping[str, Any] | None

additional headers to include in the header part. (Default value = None)

None
lifetime int | None

lifetime, in seconds, to use for the exp claim. If None, use the default_lifetime defined at initialization time.

None
leeway int | None

leeway, in seconds, to use for the nbf claim. If None, use the default_leeway defined at initialization time.

None

Returns:

Type Description
SignedJwt

the resulting signed token.

Source code in jwskate/jwt/signer.py
 84
 85
 86
 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
132
133
134
135
def sign(
    self,
    *,
    subject: str | None = None,
    audience: str | Iterable[str] | None = None,
    extra_claims: Mapping[str, Any] | None = None,
    extra_headers: Mapping[str, Any] | None = None,
    lifetime: int | None = None,
    leeway: int | None = None,
) -> SignedJwt:
    """Sign a Jwt.

    Claim 'issuer' will have the value defined at initialization time. Claim `iat`, `nbf` and `exp` will reflect
    the current time when the token is signed. `exp` includes `lifetime` seconds in the future, and `nbf`
    includes `leeway` seconds in the past.

    Args:
      subject: the subject to include in claim `sub`. (Default value = None)
      audience: the audience identifier(s) to include in claim `aud`.
      extra_claims: additional claims to include in the signed token. (Default value = None)
      extra_headers: additional headers to include in the header part. (Default value = None)
      lifetime: lifetime, in seconds, to use for the `exp` claim. If None, use the default_lifetime defined at
        initialization time.
      leeway: leeway, in seconds, to use for the `nbf` claim. If None, use the default_leeway defined at
        initialization time.

    Returns:
      the resulting signed token.

    """
    now = Jwt.timestamp()
    lifetime = lifetime or self.default_lifetime
    exp = now + lifetime
    leeway = leeway or self.default_leeway
    nbf = (now - leeway) if leeway is not None else None
    jti = self.generate_jti()
    extra_claims = extra_claims or {}
    claims = {
        key: value
        for key, value in dict(
            extra_claims,
            iss=self.issuer,
            aud=audience,
            sub=subject,
            iat=now,
            exp=exp,
            nbf=nbf,
            jti=jti,
        ).items()
        if value is not None
    }
    return Jwt.sign(claims, key=self.jwk, alg=self.alg, extra_headers=extra_headers)

generate_jti

1
generate_jti() -> str

Generate Jwt Token ID (jti) values.

Default uses UUID4. Can be overridden in subclasses.

Returns:

Type Description
str

A unique value suitable for use as JWT Token ID (jti) claim.

Source code in jwskate/jwt/signer.py
137
138
139
140
141
142
143
144
145
146
def generate_jti(self) -> str:
    """Generate Jwt Token ID (jti) values.

    Default uses UUID4. Can be overridden in subclasses.

    Returns:
        A unique value suitable for use as JWT Token ID (jti) claim.

    """
    return str(uuid.uuid4())

with_random_key classmethod

1
2
3
4
5
6
7
8
with_random_key(
    *,
    issuer: str,
    alg: str,
    default_lifetime: int = 60,
    default_leeway: int | None = None,
    kid: str | None = None
) -> JwtSigner

Initialize a JwtSigner with a randomly generated key.

Parameters:

Name Type Description Default
issuer str

the issuer identifier

required
alg str

the signing alg to use

required
default_lifetime int

lifetime for generated tokens expiration date (exp claim)

60
default_leeway int | None

leeway for generated tokens not before date (nbf claim)

None
kid str | None

key id to use for the generated key

None

Returns:

Type Description
JwtSigner

a JwtSigner initialized with a random key

Source code in jwskate/jwt/signer.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
@classmethod
def with_random_key(
    cls,
    *,
    issuer: str,
    alg: str,
    default_lifetime: int = 60,
    default_leeway: int | None = None,
    kid: str | None = None,
) -> JwtSigner:
    """Initialize a JwtSigner with a randomly generated key.

    Args:
        issuer: the issuer identifier
        alg: the signing alg to use
        default_lifetime: lifetime for generated tokens expiration date (`exp` claim)
        default_leeway: leeway for generated tokens not before date (`nbf` claim)
        kid: key id to use for the generated key

    Returns:
        a JwtSigner initialized with a random key

    """
    jwk = Jwk.generate_for_alg(alg, kid=kid).with_kid_thumbprint()
    return cls(issuer=issuer, key=jwk, alg=alg, default_lifetime=default_lifetime, default_leeway=default_leeway)

verifier

1
2
3
4
5
6
7
8
verifier(
    *,
    audience: str,
    verifiers: (
        Iterable[Callable[[SignedJwt], None]] | None
    ) = None,
    **kwargs: Any
) -> JwtVerifier

Return the matching JwtVerifier, initialized with the public key.

Source code in jwskate/jwt/signer.py
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
def verifier(
    self,
    *,
    audience: str,
    verifiers: Iterable[Callable[[SignedJwt], None]] | None = None,
    **kwargs: Any,
) -> JwtVerifier:
    """Return the matching `JwtVerifier`, initialized with the public key."""
    return JwtVerifier(
        issuer=self.issuer,
        jwkset=self.jwk.public_jwk().as_jwks(),
        alg=self.alg,
        audience=audience,
        verifiers=verifiers,
        **kwargs,
    )

JwtVerifier

A helper class to validate JWTs tokens in a real application.

Parameters:

Name Type Description Default
jwkset JwkSet | Jwk | Mapping[str, Any]

a JwkSet or Jwk which will verify the token signatures

required
issuer str | None

expected issuer value

required
audience str | None

expected audience value

None
alg str | None

expected signature alg, if there is only one

None
algs Iterable[str] | None

expected signature algs, if there are several

None
leeway int

number of seconds to allow when verifying token validity period

10
verifiers Iterable[Callable[[SignedJwt], None]] | None

additional verifiers to implement custom checks on the tokens

None
Usage
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
from jwskate import JwtVerifier

# initialize a JwtVerifier based on its expected issuer, audience, JwkSet and allowed signature algs
jwks = requests.get("https://myissuer.local/jwks").json()
verifier = JwtVerifier(
    issuer="https://myissuer.local", jwkset=jwks, audience="myapp", alg="ES256"
)

# then verify tokens
try:
    verifier.verify(
        "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL215aXNzdWVyLmxvY2FsIiwiYXVkIjoibXlhcHAiLCJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNjcyNzU5NjM0LCJleHAiOjE2NzI3NTk2OTR9.Uu5DtCnf9cwYtem8tQ4trHVgXyZBoa8fhFcGL87O2D4"
    )
    print("token is verified!")
except ValueError:
    print("token failed verification :(")
Source code in jwskate/jwt/verifier.py
 12
 13
 14
 15
 16
 17
 18
 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
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 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
132
133
134
135
136
137
138
139
140
class JwtVerifier:
    """A helper class to validate JWTs tokens in a real application.

    Args:
        jwkset: a `JwkSet` or `Jwk` which will verify the token signatures
        issuer: expected issuer value
        audience: expected audience value
        alg: expected signature alg, if there is only one
        algs: expected signature algs, if there are several
        leeway: number of seconds to allow when verifying token validity period
        verifiers: additional verifiers to implement custom checks on the tokens

    Usage:
        ```python
        from jwskate import JwtVerifier

        # initialize a JwtVerifier based on its expected issuer, audience, JwkSet and allowed signature algs
        jwks = requests.get("https://myissuer.local/jwks").json()
        verifier = JwtVerifier(
            issuer="https://myissuer.local", jwkset=jwks, audience="myapp", alg="ES256"
        )

        # then verify tokens
        try:
            verifier.verify(
                "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL215aXNzdWVyLmxvY2FsIiwiYXVkIjoibXlhcHAiLCJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNjcyNzU5NjM0LCJleHAiOjE2NzI3NTk2OTR9.Uu5DtCnf9cwYtem8tQ4trHVgXyZBoa8fhFcGL87O2D4"
            )
            print("token is verified!")
        except ValueError:
            print("token failed verification :(")
        ```

    """

    def __init__(
        self,
        jwkset: JwkSet | Jwk | Mapping[str, Any],
        *,
        issuer: str | None,
        audience: str | None = None,
        alg: str | None = None,
        algs: Iterable[str] | None = None,
        leeway: int = 10,
        verifiers: Iterable[Callable[[SignedJwt], None]] | None = None,
    ) -> None:
        if isinstance(jwkset, Jwk):
            jwkset = jwkset.as_jwks()
        elif isinstance(jwkset, dict):
            jwkset = JwkSet(jwkset) if "keys" in jwkset else Jwk(jwkset).as_jwks()

        if not isinstance(jwkset, JwkSet) or jwkset.is_private:
            msg = (
                "Please provide either a `JwkSet` or a single `Jwk` for signature verification. "
                "Signature verification keys must be public."
            )
            raise ValueError(msg)

        self.issuer = issuer
        self.jwkset = jwkset
        self.audience = audience
        self.alg = alg
        self.algs = algs
        self.leeway = leeway
        self.verifiers = list(verifiers) if verifiers else []

    def verify(self, jwt: SignedJwt | str | bytes) -> None:
        """Verify a given JWT token.

        This checks the token signature, issuer, audience and expiration date, plus any custom verification,
        as configured at init time.

        Args:
            jwt: the JWT token to verify

        """
        if not isinstance(jwt, SignedJwt):
            jwt = SignedJwt(jwt)

        if self.issuer and jwt.issuer != self.issuer:
            msg = "Mismatching issuer"
            raise InvalidClaim(msg, self.issuer, jwt.issuer)

        if self.audience and self.audience not in jwt.audiences:
            msg = "Mismatching audience"
            raise InvalidClaim(msg, self.audience, jwt.audiences)

        if "kid" in jwt.headers:
            jwk = self.jwkset.get_jwk_by_kid(jwt.kid)
            jwt.verify(jwk, alg=self.alg, algs=self.algs)
        else:
            for jwk in self.jwkset.verification_keys():
                if jwt.verify_signature(jwk, alg=self.alg, algs=self.algs):
                    break
            else:
                raise InvalidSignature(data=jwt, key=self.jwkset, alg=self.alg, algs=self.algs)

        if jwt.is_expired(self.leeway):
            msg = f"Jwt token expired at {jwt.expires_at}"
            raise ExpiredJwt(msg)

        for verifier in self.verifiers:
            verifier(jwt)

    def custom_verifier(self, verifier: Callable[[SignedJwt], None]) -> None:
        """A decorator to add custom verification steps to this verifier.

        Usage:
            ```python
            from jwskate import Jwk, JwtVerifier

            verification_key = Jwk(
                {"kty": "oct", "k": "eW91ci0yNTYtYml0LXNlY3JldA", "alg": "HS256"}
            )
            verifier = JwtVerifier(verification_key.as_jwks(), issuer="https://foo.bar")


            @verifier.custom_verifier
            def must_contain_claim_foo(jwt):
                if "foo" not in jwt:
                    raise ValueError("No foo!")


            verifier.verify(
                "eyJhbGciOiJIUzI1NiIsImtpZCI6ImlfdXRLRXhBS05jXy1hd3FEUkFVYmFoTWd5RGFLREdfTTc1S01Cd2xBdkEifQ.eyJpc3MiOiJodHRwczovL2Zvby5iYXIiLCJmb28iOiJZRVMiLCJpYXQiOjE1MTYyMzkwMjJ9.hk2vnymjcww8K-OcOkNCPUiJK-8Rj--RKJqsHSKe4jM"
            )
            ```

        """
        self.verifiers.append(verifier)

verify

1
verify(jwt: SignedJwt | str | bytes) -> None

Verify a given JWT token.

This checks the token signature, issuer, audience and expiration date, plus any custom verification, as configured at init time.

Parameters:

Name Type Description Default
jwt SignedJwt | str | bytes

the JWT token to verify

required
Source code in jwskate/jwt/verifier.py
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 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
def verify(self, jwt: SignedJwt | str | bytes) -> None:
    """Verify a given JWT token.

    This checks the token signature, issuer, audience and expiration date, plus any custom verification,
    as configured at init time.

    Args:
        jwt: the JWT token to verify

    """
    if not isinstance(jwt, SignedJwt):
        jwt = SignedJwt(jwt)

    if self.issuer and jwt.issuer != self.issuer:
        msg = "Mismatching issuer"
        raise InvalidClaim(msg, self.issuer, jwt.issuer)

    if self.audience and self.audience not in jwt.audiences:
        msg = "Mismatching audience"
        raise InvalidClaim(msg, self.audience, jwt.audiences)

    if "kid" in jwt.headers:
        jwk = self.jwkset.get_jwk_by_kid(jwt.kid)
        jwt.verify(jwk, alg=self.alg, algs=self.algs)
    else:
        for jwk in self.jwkset.verification_keys():
            if jwt.verify_signature(jwk, alg=self.alg, algs=self.algs):
                break
        else:
            raise InvalidSignature(data=jwt, key=self.jwkset, alg=self.alg, algs=self.algs)

    if jwt.is_expired(self.leeway):
        msg = f"Jwt token expired at {jwt.expires_at}"
        raise ExpiredJwt(msg)

    for verifier in self.verifiers:
        verifier(jwt)

custom_verifier

1
2
3
custom_verifier(
    verifier: Callable[[SignedJwt], None]
) -> None

A decorator to add custom verification steps to this verifier.

Usage
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
from jwskate import Jwk, JwtVerifier

verification_key = Jwk(
    {"kty": "oct", "k": "eW91ci0yNTYtYml0LXNlY3JldA", "alg": "HS256"}
)
verifier = JwtVerifier(verification_key.as_jwks(), issuer="https://foo.bar")


@verifier.custom_verifier
def must_contain_claim_foo(jwt):
    if "foo" not in jwt:
        raise ValueError("No foo!")


verifier.verify(
    "eyJhbGciOiJIUzI1NiIsImtpZCI6ImlfdXRLRXhBS05jXy1hd3FEUkFVYmFoTWd5RGFLREdfTTc1S01Cd2xBdkEifQ.eyJpc3MiOiJodHRwczovL2Zvby5iYXIiLCJmb28iOiJZRVMiLCJpYXQiOjE1MTYyMzkwMjJ9.hk2vnymjcww8K-OcOkNCPUiJK-8Rj--RKJqsHSKe4jM"
)
Source code in jwskate/jwt/verifier.py
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
def custom_verifier(self, verifier: Callable[[SignedJwt], None]) -> None:
    """A decorator to add custom verification steps to this verifier.

    Usage:
        ```python
        from jwskate import Jwk, JwtVerifier

        verification_key = Jwk(
            {"kty": "oct", "k": "eW91ci0yNTYtYml0LXNlY3JldA", "alg": "HS256"}
        )
        verifier = JwtVerifier(verification_key.as_jwks(), issuer="https://foo.bar")


        @verifier.custom_verifier
        def must_contain_claim_foo(jwt):
            if "foo" not in jwt:
                raise ValueError("No foo!")


        verifier.verify(
            "eyJhbGciOiJIUzI1NiIsImtpZCI6ImlfdXRLRXhBS05jXy1hd3FEUkFVYmFoTWd5RGFLREdfTTc1S01Cd2xBdkEifQ.eyJpc3MiOiJodHRwczovL2Zvby5iYXIiLCJmb28iOiJZRVMiLCJpYXQiOjE1MTYyMzkwMjJ9.hk2vnymjcww8K-OcOkNCPUiJK-8Rj--RKJqsHSKe4jM"
        )
        ```

    """
    self.verifiers.append(verifier)

jwskate.jws

This module implements JWS token handling.

InvalidJws

Bases: ValueError

Raised when an invalid Jws is parsed.

Source code in jwskate/jws/compact.py
20
21
class InvalidJws(ValueError):
    """Raised when an invalid Jws is parsed."""

JwsCompact

Bases: BaseCompactToken

Represents a Json Web Signature (JWS), using compact serialization, as defined in RFC7515.

Parameters:

Name Type Description Default
value bytes | str

the JWS token value

required
Source code in jwskate/jws/compact.py
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 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
132
133
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
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
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
class JwsCompact(BaseCompactToken):
    """Represents a Json Web Signature (JWS), using compact serialization, as defined in RFC7515.

    Args:
        value: the JWS token value

    """

    def __init__(self, value: bytes | str, max_size: int = 16 * 1024):
        super().__init__(value, max_size)

        parts = BinaPy(self.value).split(b".")

        if len(parts) != 3:  # noqa: PLR2004
            msg = "A JWS must contain a header, a payload and a signature, separated by dots"
            raise InvalidJws(msg)

        header, payload, signature = parts

        try:
            self.headers = header.decode_from("b64u").parse_from("json")
        except ValueError as exc:
            msg = "Invalid JWS header: it must be a Base64URL-encoded JSON object"
            raise InvalidJws(msg) from exc

        try:
            self.payload = payload.decode_from("b64u")
        except ValueError as exc:
            msg = "Invalid JWS payload: it must be a Base64URL-encoded binary data (bytes)"
            raise InvalidJws(msg) from exc

        try:
            self.signature = signature.decode_from("b64u")
        except ValueError as exc:
            msg = "Invalid JWS signature: it must be a Base64URL-encoded binary data (bytes)"
            raise InvalidJws(msg) from exc

    @classmethod
    def sign(
        cls,
        payload: bytes | SupportsBytes,
        key: Jwk | Mapping[str, Any] | Any,
        alg: str | None = None,
        extra_headers: Mapping[str, Any] | None = None,
    ) -> JwsCompact:
        """Sign a payload and returns the resulting JwsCompact.

        Args:
          payload: the payload to sign
          key: the jwk to use to sign this payload
          alg: the alg to use
          extra_headers: additional headers to add to the Jws Headers

        Returns:
          the resulting token

        """
        key = to_jwk(key)

        if not isinstance(payload, bytes):
            payload = bytes(payload)

        headers = dict(extra_headers or {}, alg=alg)
        kid = key.get("kid")
        if kid:
            headers["kid"] = kid

        signed_part = JwsSignature.assemble_signed_part(headers, payload)
        signature = key.sign(signed_part, alg=alg)
        return cls.from_parts(signed_part, signature)

    @classmethod
    def from_parts(
        cls,
        signed_part: bytes | SupportsBytes | str,
        signature: bytes | SupportsBytes,
    ) -> JwsCompact:
        """Construct a JWS token based on its signed part and signature values.

        Signed part is the concatenation of the header and payload, both encoded in Base64-Url, and joined by a dot.

        Args:
          signed_part: the signed part
          signature: the signature value

        Returns:
            the resulting token

        """
        if isinstance(signed_part, str):
            signed_part = signed_part.encode("ascii")
        if not isinstance(signed_part, bytes):
            signed_part = bytes(signed_part)

        if not isinstance(signature, bytes):
            signature = bytes(signature)

        return cls(b".".join((signed_part, BinaPy(signature).to("b64u"))))

    @cached_property
    def signed_part(self) -> bytes:
        """Returns the signed part (header + payload) from this JwsCompact.

        Returns:
            the signed part

        """
        return b".".join(self.value.split(b".", 2)[:2])

    def verify_signature(
        self,
        key: Jwk | Mapping[str, Any] | Any,
        *,
        alg: str | None = None,
        algs: Iterable[str] | None = None,
    ) -> bool:
        """Verify the signature from this JwsCompact using a key.

        Args:
          key: the Jwk to use to validate this signature
          alg: the alg to use, if there is only 1 allowed
          algs: the allowed algs, if here are several

        Returns:
         `True` if the signature matches, `False` otherwise

        """
        key = to_jwk(key)
        return key.verify(self.signed_part, self.signature, alg=alg, algs=algs)

    def verify(
        self,
        key: Jwk | Mapping[str, Any] | Any,
        *,
        alg: str | None = None,
        algs: Iterable[str] | None = None,
    ) -> Self:
        """Verify this JWS signature.

        This is an alternative to `.verify_signature()` that raises an exception if the signature is not
        verified.

        Args:
          key: the Jwk to use to validate this signature
          alg: the alg to use, if there is only 1 allowed
          algs: the allowed algs, if here are several

        Raises:
            InvalidSignature: if the signature does not verify

        Returns:
          The same JwsCompact

        Usage:
            ```python
            jws = JwsCompact(
                "eyJhbGciOm51bGx9.SGVsbG8gV29ybGQh.rd61m4AQ6dOqexdZC9revgictOzRd7dmHiQ5UMa9g66BhAO8crw_E_5SkydE-PNNzRkdFdq4P2YzzM1HgfnWlw"
            ).verify(
                {
                    "kty": "EC",
                    "alg": "ES256",
                    "crv": "P-256",
                    "x": "T_RLrReYRPIknDpIEjLUoy7ibAbqJDfHe03mkEjI_oU",
                    "y": "8MM4v58j8IHag6uibgC0Qn275bl9c9JR0UD0TwFgMPM",
                }
            )

            assert jws.payload == b"Hello World!"
            ```

        """
        if self.verify_signature(key, alg=alg, algs=algs):
            return self
        raise InvalidSignature(data=self, key=key, alg=alg, algs=algs)

    def flat_json(self, unprotected_header: Any = None) -> JwsJsonFlat:
        """Create a JWS in JSON flat format based on this Compact JWS.

        Args:
          unprotected_header: optional unprotected header to include in the JWS JSON

        Returns:
            the resulting token

        """
        from .json import JwsJsonFlat

        protected, payload, signature = self.value.split(b".")

        content = {
            "payload": payload.decode(),
            "protected": protected.decode(),
            "signature": signature.decode(),
        }
        if unprotected_header is not None:
            content["header"] = unprotected_header
        return JwsJsonFlat(content)

    def general_json(self, unprotected_header: Any = None) -> JwsJsonGeneral:
        """Create a JWS in JSON General format based on this JWS Compact.

        The resulting token will have a single signature which is the one from this token.

        Args:
            unprotected_header: optional unprotected header to include in the JWS JSON

        Returns:
            the resulting token

        """
        jws = self.flat_json(unprotected_header)
        return jws.generalize()

    def jws_signature(self, unprotected_header: Any = None) -> JwsSignature:
        """Return a JwsSignature based on this JWS Compact token."""
        return JwsSignature.from_parts(protected=self.headers, signature=self.signature, header=unprotected_header)

signed_part cached property

1
signed_part: bytes

Returns the signed part (header + payload) from this JwsCompact.

Returns:

Type Description
bytes

the signed part

sign classmethod

1
2
3
4
5
6
sign(
    payload: bytes | SupportsBytes,
    key: Jwk | Mapping[str, Any] | Any,
    alg: str | None = None,
    extra_headers: Mapping[str, Any] | None = None,
) -> JwsCompact

Sign a payload and returns the resulting JwsCompact.

Parameters:

Name Type Description Default
payload bytes | SupportsBytes

the payload to sign

required
key Jwk | Mapping[str, Any] | Any

the jwk to use to sign this payload

required
alg str | None

the alg to use

None
extra_headers Mapping[str, Any] | None

additional headers to add to the Jws Headers

None

Returns:

Type Description
JwsCompact

the resulting token

Source code in jwskate/jws/compact.py
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
@classmethod
def sign(
    cls,
    payload: bytes | SupportsBytes,
    key: Jwk | Mapping[str, Any] | Any,
    alg: str | None = None,
    extra_headers: Mapping[str, Any] | None = None,
) -> JwsCompact:
    """Sign a payload and returns the resulting JwsCompact.

    Args:
      payload: the payload to sign
      key: the jwk to use to sign this payload
      alg: the alg to use
      extra_headers: additional headers to add to the Jws Headers

    Returns:
      the resulting token

    """
    key = to_jwk(key)

    if not isinstance(payload, bytes):
        payload = bytes(payload)

    headers = dict(extra_headers or {}, alg=alg)
    kid = key.get("kid")
    if kid:
        headers["kid"] = kid

    signed_part = JwsSignature.assemble_signed_part(headers, payload)
    signature = key.sign(signed_part, alg=alg)
    return cls.from_parts(signed_part, signature)

from_parts classmethod

1
2
3
4
from_parts(
    signed_part: bytes | SupportsBytes | str,
    signature: bytes | SupportsBytes,
) -> JwsCompact

Construct a JWS token based on its signed part and signature values.

Signed part is the concatenation of the header and payload, both encoded in Base64-Url, and joined by a dot.

Parameters:

Name Type Description Default
signed_part bytes | SupportsBytes | str

the signed part

required
signature bytes | SupportsBytes

the signature value

required

Returns:

Type Description
JwsCompact

the resulting token

Source code in jwskate/jws/compact.py
 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
@classmethod
def from_parts(
    cls,
    signed_part: bytes | SupportsBytes | str,
    signature: bytes | SupportsBytes,
) -> JwsCompact:
    """Construct a JWS token based on its signed part and signature values.

    Signed part is the concatenation of the header and payload, both encoded in Base64-Url, and joined by a dot.

    Args:
      signed_part: the signed part
      signature: the signature value

    Returns:
        the resulting token

    """
    if isinstance(signed_part, str):
        signed_part = signed_part.encode("ascii")
    if not isinstance(signed_part, bytes):
        signed_part = bytes(signed_part)

    if not isinstance(signature, bytes):
        signature = bytes(signature)

    return cls(b".".join((signed_part, BinaPy(signature).to("b64u"))))

verify_signature

1
2
3
4
5
6
verify_signature(
    key: Jwk | Mapping[str, Any] | Any,
    *,
    alg: str | None = None,
    algs: Iterable[str] | None = None
) -> bool

Verify the signature from this JwsCompact using a key.

Parameters:

Name Type Description Default
key Jwk | Mapping[str, Any] | Any

the Jwk to use to validate this signature

required
alg str | None

the alg to use, if there is only 1 allowed

None
algs Iterable[str] | None

the allowed algs, if here are several

None

Returns:

Type Description
bool

True if the signature matches, False otherwise

Source code in jwskate/jws/compact.py
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
def verify_signature(
    self,
    key: Jwk | Mapping[str, Any] | Any,
    *,
    alg: str | None = None,
    algs: Iterable[str] | None = None,
) -> bool:
    """Verify the signature from this JwsCompact using a key.

    Args:
      key: the Jwk to use to validate this signature
      alg: the alg to use, if there is only 1 allowed
      algs: the allowed algs, if here are several

    Returns:
     `True` if the signature matches, `False` otherwise

    """
    key = to_jwk(key)
    return key.verify(self.signed_part, self.signature, alg=alg, algs=algs)

verify

1
2
3
4
5
6
verify(
    key: Jwk | Mapping[str, Any] | Any,
    *,
    alg: str | None = None,
    algs: Iterable[str] | None = None
) -> Self

Verify this JWS signature.

This is an alternative to .verify_signature() that raises an exception if the signature is not verified.

Parameters:

Name Type Description Default
key Jwk | Mapping[str, Any] | Any

the Jwk to use to validate this signature

required
alg str | None

the alg to use, if there is only 1 allowed

None
algs Iterable[str] | None

the allowed algs, if here are several

None

Raises:

Type Description
InvalidSignature

if the signature does not verify

Returns:

Type Description
Self

The same JwsCompact

Usage
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
jws = JwsCompact(
    "eyJhbGciOm51bGx9.SGVsbG8gV29ybGQh.rd61m4AQ6dOqexdZC9revgictOzRd7dmHiQ5UMa9g66BhAO8crw_E_5SkydE-PNNzRkdFdq4P2YzzM1HgfnWlw"
).verify(
    {
        "kty": "EC",
        "alg": "ES256",
        "crv": "P-256",
        "x": "T_RLrReYRPIknDpIEjLUoy7ibAbqJDfHe03mkEjI_oU",
        "y": "8MM4v58j8IHag6uibgC0Qn275bl9c9JR0UD0TwFgMPM",
    }
)

assert jws.payload == b"Hello World!"
Source code in jwskate/jws/compact.py
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
def verify(
    self,
    key: Jwk | Mapping[str, Any] | Any,
    *,
    alg: str | None = None,
    algs: Iterable[str] | None = None,
) -> Self:
    """Verify this JWS signature.

    This is an alternative to `.verify_signature()` that raises an exception if the signature is not
    verified.

    Args:
      key: the Jwk to use to validate this signature
      alg: the alg to use, if there is only 1 allowed
      algs: the allowed algs, if here are several

    Raises:
        InvalidSignature: if the signature does not verify

    Returns:
      The same JwsCompact

    Usage:
        ```python
        jws = JwsCompact(
            "eyJhbGciOm51bGx9.SGVsbG8gV29ybGQh.rd61m4AQ6dOqexdZC9revgictOzRd7dmHiQ5UMa9g66BhAO8crw_E_5SkydE-PNNzRkdFdq4P2YzzM1HgfnWlw"
        ).verify(
            {
                "kty": "EC",
                "alg": "ES256",
                "crv": "P-256",
                "x": "T_RLrReYRPIknDpIEjLUoy7ibAbqJDfHe03mkEjI_oU",
                "y": "8MM4v58j8IHag6uibgC0Qn275bl9c9JR0UD0TwFgMPM",
            }
        )

        assert jws.payload == b"Hello World!"
        ```

    """
    if self.verify_signature(key, alg=alg, algs=algs):
        return self
    raise InvalidSignature(data=self, key=key, alg=alg, algs=algs)

flat_json

1
flat_json(unprotected_header: Any = None) -> JwsJsonFlat

Create a JWS in JSON flat format based on this Compact JWS.

Parameters:

Name Type Description Default
unprotected_header Any

optional unprotected header to include in the JWS JSON

None

Returns:

Type Description
JwsJsonFlat

the resulting token

Source code in jwskate/jws/compact.py
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
def flat_json(self, unprotected_header: Any = None) -> JwsJsonFlat:
    """Create a JWS in JSON flat format based on this Compact JWS.

    Args:
      unprotected_header: optional unprotected header to include in the JWS JSON

    Returns:
        the resulting token

    """
    from .json import JwsJsonFlat

    protected, payload, signature = self.value.split(b".")

    content = {
        "payload": payload.decode(),
        "protected": protected.decode(),
        "signature": signature.decode(),
    }
    if unprotected_header is not None:
        content["header"] = unprotected_header
    return JwsJsonFlat(content)

general_json

1
2
3
general_json(
    unprotected_header: Any = None,
) -> JwsJsonGeneral

Create a JWS in JSON General format based on this JWS Compact.

The resulting token will have a single signature which is the one from this token.

Parameters:

Name Type Description Default
unprotected_header Any

optional unprotected header to include in the JWS JSON

None

Returns:

Type Description
JwsJsonGeneral

the resulting token

Source code in jwskate/jws/compact.py
222
223
224
225
226
227
228
229
230
231
232
233
234
235
def general_json(self, unprotected_header: Any = None) -> JwsJsonGeneral:
    """Create a JWS in JSON General format based on this JWS Compact.

    The resulting token will have a single signature which is the one from this token.

    Args:
        unprotected_header: optional unprotected header to include in the JWS JSON

    Returns:
        the resulting token

    """
    jws = self.flat_json(unprotected_header)
    return jws.generalize()

jws_signature

1
2
3
jws_signature(
    unprotected_header: Any = None,
) -> JwsSignature

Return a JwsSignature based on this JWS Compact token.

Source code in jwskate/jws/compact.py
237
238
239
def jws_signature(self, unprotected_header: Any = None) -> JwsSignature:
    """Return a JwsSignature based on this JWS Compact token."""
    return JwsSignature.from_parts(protected=self.headers, signature=self.signature, header=unprotected_header)

JwsJsonFlat

Bases: JwsSignature

Represent a JWS with a single signature in JSON flat format.

Source code in jwskate/jws/json.py
 17
 18
 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
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 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
132
133
134
class JwsJsonFlat(JwsSignature):
    """Represent a JWS with a single signature in JSON flat format."""

    @cached_property
    def payload(self) -> bytes:
        """The JWS payload, decoded.

        Returns:
            The raw JWS payload.

        """
        payload = self.get("payload")
        if payload is None:
            msg = "This Jws JSON does not contain a 'payload' member"
            raise AttributeError(msg)
        return BinaPy(payload).decode_from("b64u")

    @cached_property
    def jws_signature(self) -> JwsSignature:
        """The JWS signature.

        Returns:
            The JWS signature.

        """
        content = {
            "protected": self["protected"],
            "signature": self["signature"],
        }
        header = self.get("header")
        if header:
            content["header"] = self.header
        return JwsSignature(content)

    @classmethod
    def sign(
        cls,
        payload: bytes,
        key: Jwk | Mapping[str, Any] | Any,
        alg: str | None = None,
        extra_protected_headers: Mapping[str, Any] | None = None,
        header: Any | None = None,
        **kwargs: Any,
    ) -> JwsJsonFlat:
        """Signs a payload into a JWS in JSON flat format.

        Args:
            payload: the data to sign.
            key: the key to use
            alg: the signature alg to use
            extra_protected_headers: additional protected headers to include
            header: the unprotected header to include
            **kwargs: extra attributes to include in the JWS

        Returns:
            The JWS with the payload, signature, header and extra claims.

        """
        signature = super().sign(payload, key, alg, extra_protected_headers, header, **kwargs)
        signature["payload"] = BinaPy(payload).to("b64u").ascii()
        return cls(signature)

    def generalize(self) -> JwsJsonGeneral:
        """Create a JWS in JSON general format from this JWS in JSON flat.

        Returns:
            A JwsJsonGeneral with the same payload and signature.

        """
        content = self.copy()
        protected = content.pop("protected")
        header = content.pop("header", None)
        signature = content.pop("signature")
        jws_signature = {"protected": protected, "signature": signature}
        if header is not None:
            jws_signature["header"] = header
        content["signatures"] = [jws_signature]
        return JwsJsonGeneral(content)

    def signed_part(self) -> bytes:
        """Return the signed part from this JWS, as bytes.

        This is a concatenation of the protected header and the payload, separated by a dot (`.`).

        Returns:
            The signed data part.

        """
        return JwsSignature.assemble_signed_part(self.protected, self.payload)

    def compact(self) -> JwsCompact:
        """Create a JWS in compact format from this JWS JSON.

        Returns:
            A `JwsCompact` with the same payload and signature.

        """
        return JwsCompact.from_parts(self.signed_part(), self.signature)

    def verify_signature(
        self,
        key: Jwk | Mapping[str, Any] | Any,
        *,
        alg: str | None = None,
        algs: Iterable[str] | None = None,
    ) -> bool:
        """Verify this JWS signature with a given key.

        Args:
            key: the key to use to validate this signature.
            alg: the signature alg, if only 1 is allowed.
            algs: the allowed signature algs, if there are several.

        Returns:
            `True` if the signature is verified, `False` otherwise.

        """
        return self.jws_signature.verify(self.payload, key, alg=alg, algs=algs)

payload cached property

1
payload: bytes

The JWS payload, decoded.

Returns:

Type Description
bytes

The raw JWS payload.

jws_signature cached property

1
jws_signature: JwsSignature

The JWS signature.

Returns:

Type Description
JwsSignature

The JWS signature.

sign classmethod

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
sign(
    payload: bytes,
    key: Jwk | Mapping[str, Any] | Any,
    alg: str | None = None,
    extra_protected_headers: (
        Mapping[str, Any] | None
    ) = None,
    header: Any | None = None,
    **kwargs: Any
) -> JwsJsonFlat

Signs a payload into a JWS in JSON flat format.

Parameters:

Name Type Description Default
payload bytes

the data to sign.

required
key Jwk | Mapping[str, Any] | Any

the key to use

required
alg str | None

the signature alg to use

None
extra_protected_headers Mapping[str, Any] | None

additional protected headers to include

None
header Any | None

the unprotected header to include

None
**kwargs Any

extra attributes to include in the JWS

{}

Returns:

Type Description
JwsJsonFlat

The JWS with the payload, signature, header and extra claims.

Source code in jwskate/jws/json.py
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
@classmethod
def sign(
    cls,
    payload: bytes,
    key: Jwk | Mapping[str, Any] | Any,
    alg: str | None = None,
    extra_protected_headers: Mapping[str, Any] | None = None,
    header: Any | None = None,
    **kwargs: Any,
) -> JwsJsonFlat:
    """Signs a payload into a JWS in JSON flat format.

    Args:
        payload: the data to sign.
        key: the key to use
        alg: the signature alg to use
        extra_protected_headers: additional protected headers to include
        header: the unprotected header to include
        **kwargs: extra attributes to include in the JWS

    Returns:
        The JWS with the payload, signature, header and extra claims.

    """
    signature = super().sign(payload, key, alg, extra_protected_headers, header, **kwargs)
    signature["payload"] = BinaPy(payload).to("b64u").ascii()
    return cls(signature)

generalize

1
generalize() -> JwsJsonGeneral

Create a JWS in JSON general format from this JWS in JSON flat.

Returns:

Type Description
JwsJsonGeneral

A JwsJsonGeneral with the same payload and signature.

Source code in jwskate/jws/json.py
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
def generalize(self) -> JwsJsonGeneral:
    """Create a JWS in JSON general format from this JWS in JSON flat.

    Returns:
        A JwsJsonGeneral with the same payload and signature.

    """
    content = self.copy()
    protected = content.pop("protected")
    header = content.pop("header", None)
    signature = content.pop("signature")
    jws_signature = {"protected": protected, "signature": signature}
    if header is not None:
        jws_signature["header"] = header
    content["signatures"] = [jws_signature]
    return JwsJsonGeneral(content)

signed_part

1
signed_part() -> bytes

Return the signed part from this JWS, as bytes.

This is a concatenation of the protected header and the payload, separated by a dot (.).

Returns:

Type Description
bytes

The signed data part.

Source code in jwskate/jws/json.py
 96
 97
 98
 99
100
101
102
103
104
105
def signed_part(self) -> bytes:
    """Return the signed part from this JWS, as bytes.

    This is a concatenation of the protected header and the payload, separated by a dot (`.`).

    Returns:
        The signed data part.

    """
    return JwsSignature.assemble_signed_part(self.protected, self.payload)

compact

1
compact() -> JwsCompact

Create a JWS in compact format from this JWS JSON.

Returns:

Type Description
JwsCompact

A JwsCompact with the same payload and signature.

Source code in jwskate/jws/json.py
107
108
109
110
111
112
113
114
def compact(self) -> JwsCompact:
    """Create a JWS in compact format from this JWS JSON.

    Returns:
        A `JwsCompact` with the same payload and signature.

    """
    return JwsCompact.from_parts(self.signed_part(), self.signature)

verify_signature

1
2
3
4
5
6
verify_signature(
    key: Jwk | Mapping[str, Any] | Any,
    *,
    alg: str | None = None,
    algs: Iterable[str] | None = None
) -> bool

Verify this JWS signature with a given key.

Parameters:

Name Type Description Default
key Jwk | Mapping[str, Any] | Any

the key to use to validate this signature.

required
alg str | None

the signature alg, if only 1 is allowed.

None
algs Iterable[str] | None

the allowed signature algs, if there are several.

None

Returns:

Type Description
bool

True if the signature is verified, False otherwise.

Source code in jwskate/jws/json.py
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
def verify_signature(
    self,
    key: Jwk | Mapping[str, Any] | Any,
    *,
    alg: str | None = None,
    algs: Iterable[str] | None = None,
) -> bool:
    """Verify this JWS signature with a given key.

    Args:
        key: the key to use to validate this signature.
        alg: the signature alg, if only 1 is allowed.
        algs: the allowed signature algs, if there are several.

    Returns:
        `True` if the signature is verified, `False` otherwise.

    """
    return self.jws_signature.verify(self.payload, key, alg=alg, algs=algs)

JwsJsonGeneral

Bases: BaseJsonDict

Represents a JWS in JSON general format (possibly with multiple signatures).

Source code in jwskate/jws/json.py
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
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
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
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
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
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
class JwsJsonGeneral(BaseJsonDict):
    """Represents a JWS in JSON general format (possibly with multiple signatures)."""

    @cached_property
    def payload(self) -> bytes:
        """The raw signed data.

        Returns:
            The signed data.

        """
        payload = self.get("payload")
        if payload is None:
            msg = "This Jws JSON does not contain a 'payload' member"
            raise AttributeError(msg)
        return BinaPy(payload).decode_from("b64u")

    @classmethod
    def sign(
        cls,
        payload: bytes,
        *signature_parameters: (
            tuple[
                Jwk | Mapping[str, Any],
                str,
                Mapping[str, Any] | None,
                Mapping[str, Any] | None,
            ]
            | tuple[
                Jwk | Mapping[str, Any],
                str,
                Mapping[str, Any] | None,
            ]
            | tuple[
                Jwk | Mapping[str, Any],
                str,
            ]
            | Jwk
            | Mapping[str, Any]
        ),
    ) -> JwsJsonGeneral:
        """Sign a payload with several keys and return the resulting JWS in JSON general format.

        Args:
            payload: the data to sign
            *signature_parameters: each of those parameter can be:

                - a `(jwk, alg, extra_protected_headers, header)` tuple
                - a `(jwk, alg, extra_protected_headers)` tuple,
                - a `(jwk, alg)` tuple,
                - a `jwk`

                with:

                - `jwk` being a `Jwk` key,
                - `alg` being the signature algorithm to use,
                - `extra_protected_headers` a mapping of extra protected headers and values to include,
                - `header` the raw unprotected header to include in the signature.

        Returns:
            the generated signatures in JSON General format.

        """
        jws = cls({"payload": BinaPy(payload).to("b64u").ascii()})
        for parameters in signature_parameters:
            jws.add_signature(*parameters)
        return jws

    @cached_property
    def signatures(self) -> list[JwsSignature]:
        """The list of `JwsSignature` from this JWS.

        Returns:
            The list of signatures from this JWS.

        """
        signatures = self.get("signatures")
        if signatures is None:
            msg = "This Jws JSON does not contain a 'signatures' member"
            raise AttributeError(msg)
        return [JwsSignature(sig) for sig in signatures]

    def add_signature(
        self,
        key: Jwk | Mapping[str, Any] | Any,
        alg: str | None = None,
        extra_protected_headers: Mapping[str, Any] | None = None,
        header: Mapping[str, Any] | None = None,
    ) -> JwsJsonGeneral:
        """Add a new signature in this JWS.

        Args:
            key: the private key to use
            alg: the signature algorithm
            extra_protected_headers: additional headers to include, as a {key: value} mapping
            header: the raw unprotected header to include in the signature

        Returns:
            the same JWS with the new signature included.

        """
        self.setdefault("signatures", [])
        self["signatures"].append(JwsSignature.sign(self.payload, key, alg, extra_protected_headers, header))
        return self

    def signed_part(
        self,
        signature_chooser: Callable[[list[JwsSignature]], JwsSignature] = lambda sigs: sigs[0],
    ) -> bytes:
        """Return the signed part from a given signature.

        The signed part is a concatenation of the protected header from a specific signature, then the payload,
        separated by a dot (`.`).

        You can select the specific signature with the `signature_chooser` parameter.
        By default, the first signature is selected.

        Args:
            signature_chooser: a callable that takes the list of signatures from this JWS as parameter,
                and returns the chosen signature.

        Returns:
            The raw signed part from the chosen signature.

        """
        signature = signature_chooser(self.signatures)
        return JwsSignature.assemble_signed_part(signature.protected, self.payload)

    def compact(
        self,
        signature_chooser: Callable[[list[JwsSignature]], JwsSignature] = lambda sigs: sigs[0],
    ) -> JwsCompact:
        """Create a compact JWS from a specific signature from this JWS.

        Args:
            signature_chooser: a callable that takes the list of signatures from this JWS as parameter
                and returns the choosen signature.

        Returns:
            A JwsCompact with the payload and the chosen signature from this JWS.

        """
        signature = signature_chooser(self.signatures)
        return JwsCompact.from_parts(
            JwsSignature.assemble_signed_part(signature.protected, self.payload),
            signature.signature,
        )

    def flatten(
        self,
        signature_chooser: Callable[[list[JwsSignature]], JwsSignature] = lambda sigs: sigs[0],
    ) -> JwsJsonFlat:
        """Create a JWS in JSON flat format from a specific signature from this JWS.

        Args:
            signature_chooser:  a callable that takes the list of signatures from this JWS as parameter
                and returns the choosen signature.

        Returns:
            A JwsJsonFlat with the payload and the chosen signature from this JWS.

        """
        signature = signature_chooser(self.signatures)
        return JwsJsonFlat.from_parts(
            payload=self["payload"],
            protected=signature.protected,
            header=signature.header,
            signature=signature.signature,
        )

    def verify_signature(
        self,
        key: Jwk | Mapping[str, Any] | Any,
        *,
        alg: str | None = None,
        algs: Iterable[str] | None = None,
    ) -> bool:
        """Verify the signatures from this JWS.

        It tries to validate each signature with the given key, and returns `True` if at least one signature verifies.

        Args:
            key: the public key to use
            alg: the signature algorithm to use, if only 1 is allowed.
            algs: the allowed signature algorithms, if there are several.

        Returns:
            `True` if any of the signature verifies with the given key, `False` otherwise.

        """
        return any(signature.verify(self.payload, key, alg=alg, algs=algs) for signature in self.signatures)

payload cached property

1
payload: bytes

The raw signed data.

Returns:

Type Description
bytes

The signed data.

signatures cached property

1
signatures: list[JwsSignature]

The list of JwsSignature from this JWS.

Returns:

Type Description
list[JwsSignature]

The list of signatures from this JWS.

sign classmethod

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
sign(
    payload: bytes,
    *signature_parameters: tuple[
        Jwk | Mapping[str, Any],
        str,
        Mapping[str, Any] | None,
        Mapping[str, Any] | None,
    ]
    | tuple[
        Jwk | Mapping[str, Any],
        str,
        Mapping[str, Any] | None,
    ]
    | tuple[Jwk | Mapping[str, Any], str]
    | Jwk
    | Mapping[str, Any]
) -> JwsJsonGeneral

Sign a payload with several keys and return the resulting JWS in JSON general format.

Parameters:

Name Type Description Default
payload bytes

the data to sign

required
*signature_parameters tuple[Jwk | Mapping[str, Any], str, Mapping[str, Any] | None, Mapping[str, Any] | None] | tuple[Jwk | Mapping[str, Any], str, Mapping[str, Any] | None] | tuple[Jwk | Mapping[str, Any], str] | Jwk | Mapping[str, Any]

each of those parameter can be:

  • a (jwk, alg, extra_protected_headers, header) tuple
  • a (jwk, alg, extra_protected_headers) tuple,
  • a (jwk, alg) tuple,
  • a jwk

with:

  • jwk being a Jwk key,
  • alg being the signature algorithm to use,
  • extra_protected_headers a mapping of extra protected headers and values to include,
  • header the raw unprotected header to include in the signature.
()

Returns:

Type Description
JwsJsonGeneral

the generated signatures in JSON General format.

Source code in jwskate/jws/json.py
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
@classmethod
def sign(
    cls,
    payload: bytes,
    *signature_parameters: (
        tuple[
            Jwk | Mapping[str, Any],
            str,
            Mapping[str, Any] | None,
            Mapping[str, Any] | None,
        ]
        | tuple[
            Jwk | Mapping[str, Any],
            str,
            Mapping[str, Any] | None,
        ]
        | tuple[
            Jwk | Mapping[str, Any],
            str,
        ]
        | Jwk
        | Mapping[str, Any]
    ),
) -> JwsJsonGeneral:
    """Sign a payload with several keys and return the resulting JWS in JSON general format.

    Args:
        payload: the data to sign
        *signature_parameters: each of those parameter can be:

            - a `(jwk, alg, extra_protected_headers, header)` tuple
            - a `(jwk, alg, extra_protected_headers)` tuple,
            - a `(jwk, alg)` tuple,
            - a `jwk`

            with:

            - `jwk` being a `Jwk` key,
            - `alg` being the signature algorithm to use,
            - `extra_protected_headers` a mapping of extra protected headers and values to include,
            - `header` the raw unprotected header to include in the signature.

    Returns:
        the generated signatures in JSON General format.

    """
    jws = cls({"payload": BinaPy(payload).to("b64u").ascii()})
    for parameters in signature_parameters:
        jws.add_signature(*parameters)
    return jws

add_signature

1
2
3
4
5
6
7
8
add_signature(
    key: Jwk | Mapping[str, Any] | Any,
    alg: str | None = None,
    extra_protected_headers: (
        Mapping[str, Any] | None
    ) = None,
    header: Mapping[str, Any] | None = None,
) -> JwsJsonGeneral

Add a new signature in this JWS.

Parameters:

Name Type Description Default
key Jwk | Mapping[str, Any] | Any

the private key to use

required
alg str | None

the signature algorithm

None
extra_protected_headers Mapping[str, Any] | None

additional headers to include, as a {key: value} mapping

None
header Mapping[str, Any] | None

the raw unprotected header to include in the signature

None

Returns:

Type Description
JwsJsonGeneral

the same JWS with the new signature included.

Source code in jwskate/jws/json.py
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
def add_signature(
    self,
    key: Jwk | Mapping[str, Any] | Any,
    alg: str | None = None,
    extra_protected_headers: Mapping[str, Any] | None = None,
    header: Mapping[str, Any] | None = None,
) -> JwsJsonGeneral:
    """Add a new signature in this JWS.

    Args:
        key: the private key to use
        alg: the signature algorithm
        extra_protected_headers: additional headers to include, as a {key: value} mapping
        header: the raw unprotected header to include in the signature

    Returns:
        the same JWS with the new signature included.

    """
    self.setdefault("signatures", [])
    self["signatures"].append(JwsSignature.sign(self.payload, key, alg, extra_protected_headers, header))
    return self

signed_part

1
2
3
4
5
signed_part(
    signature_chooser: Callable[
        [list[JwsSignature]], JwsSignature
    ] = lambda: sigs[0]
) -> bytes

Return the signed part from a given signature.

The signed part is a concatenation of the protected header from a specific signature, then the payload, separated by a dot (.).

You can select the specific signature with the signature_chooser parameter. By default, the first signature is selected.

Parameters:

Name Type Description Default
signature_chooser Callable[[list[JwsSignature]], JwsSignature]

a callable that takes the list of signatures from this JWS as parameter, and returns the chosen signature.

lambda : sigs[0]

Returns:

Type Description
bytes

The raw signed part from the chosen signature.

Source code in jwskate/jws/json.py
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
def signed_part(
    self,
    signature_chooser: Callable[[list[JwsSignature]], JwsSignature] = lambda sigs: sigs[0],
) -> bytes:
    """Return the signed part from a given signature.

    The signed part is a concatenation of the protected header from a specific signature, then the payload,
    separated by a dot (`.`).

    You can select the specific signature with the `signature_chooser` parameter.
    By default, the first signature is selected.

    Args:
        signature_chooser: a callable that takes the list of signatures from this JWS as parameter,
            and returns the chosen signature.

    Returns:
        The raw signed part from the chosen signature.

    """
    signature = signature_chooser(self.signatures)
    return JwsSignature.assemble_signed_part(signature.protected, self.payload)

compact

1
2
3
4
5
compact(
    signature_chooser: Callable[
        [list[JwsSignature]], JwsSignature
    ] = lambda: sigs[0]
) -> JwsCompact

Create a compact JWS from a specific signature from this JWS.

Parameters:

Name Type Description Default
signature_chooser Callable[[list[JwsSignature]], JwsSignature]

a callable that takes the list of signatures from this JWS as parameter and returns the choosen signature.

lambda : sigs[0]

Returns:

Type Description
JwsCompact

A JwsCompact with the payload and the chosen signature from this JWS.

Source code in jwskate/jws/json.py
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
def compact(
    self,
    signature_chooser: Callable[[list[JwsSignature]], JwsSignature] = lambda sigs: sigs[0],
) -> JwsCompact:
    """Create a compact JWS from a specific signature from this JWS.

    Args:
        signature_chooser: a callable that takes the list of signatures from this JWS as parameter
            and returns the choosen signature.

    Returns:
        A JwsCompact with the payload and the chosen signature from this JWS.

    """
    signature = signature_chooser(self.signatures)
    return JwsCompact.from_parts(
        JwsSignature.assemble_signed_part(signature.protected, self.payload),
        signature.signature,
    )

flatten

1
2
3
4
5
flatten(
    signature_chooser: Callable[
        [list[JwsSignature]], JwsSignature
    ] = lambda: sigs[0]
) -> JwsJsonFlat

Create a JWS in JSON flat format from a specific signature from this JWS.

Parameters:

Name Type Description Default
signature_chooser Callable[[list[JwsSignature]], JwsSignature]

a callable that takes the list of signatures from this JWS as parameter and returns the choosen signature.

lambda : sigs[0]

Returns:

Type Description
JwsJsonFlat

A JwsJsonFlat with the payload and the chosen signature from this JWS.

Source code in jwskate/jws/json.py
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
def flatten(
    self,
    signature_chooser: Callable[[list[JwsSignature]], JwsSignature] = lambda sigs: sigs[0],
) -> JwsJsonFlat:
    """Create a JWS in JSON flat format from a specific signature from this JWS.

    Args:
        signature_chooser:  a callable that takes the list of signatures from this JWS as parameter
            and returns the choosen signature.

    Returns:
        A JwsJsonFlat with the payload and the chosen signature from this JWS.

    """
    signature = signature_chooser(self.signatures)
    return JwsJsonFlat.from_parts(
        payload=self["payload"],
        protected=signature.protected,
        header=signature.header,
        signature=signature.signature,
    )

verify_signature

1
2
3
4
5
6
verify_signature(
    key: Jwk | Mapping[str, Any] | Any,
    *,
    alg: str | None = None,
    algs: Iterable[str] | None = None
) -> bool

Verify the signatures from this JWS.

It tries to validate each signature with the given key, and returns True if at least one signature verifies.

Parameters:

Name Type Description Default
key Jwk | Mapping[str, Any] | Any

the public key to use

required
alg str | None

the signature algorithm to use, if only 1 is allowed.

None
algs Iterable[str] | None

the allowed signature algorithms, if there are several.

None

Returns:

Type Description
bool

True if any of the signature verifies with the given key, False otherwise.

Source code in jwskate/jws/json.py
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
def verify_signature(
    self,
    key: Jwk | Mapping[str, Any] | Any,
    *,
    alg: str | None = None,
    algs: Iterable[str] | None = None,
) -> bool:
    """Verify the signatures from this JWS.

    It tries to validate each signature with the given key, and returns `True` if at least one signature verifies.

    Args:
        key: the public key to use
        alg: the signature algorithm to use, if only 1 is allowed.
        algs: the allowed signature algorithms, if there are several.

    Returns:
        `True` if any of the signature verifies with the given key, `False` otherwise.

    """
    return any(signature.verify(self.payload, key, alg=alg, algs=algs) for signature in self.signatures)

InvalidSignature

Bases: ValueError

Raised when trying to validate a token with an invalid signature.

Source code in jwskate/jws/signature.py
14
15
16
17
18
19
20
21
class InvalidSignature(ValueError):
    """Raised when trying to validate a token with an invalid signature."""

    def __init__(self, data: SupportsBytes, key: Any, alg: str | None, algs: Iterable[str] | None) -> None:
        self.data = data
        self.key = key
        self.alg = alg
        self.algs = algs

JwsSignature

Bases: BaseJsonDict

Represent a JWS Signature.

A JWS Signature has:

  • a protected header (as a JSON object)
  • a signature value (as raw data)
  • an unprotected header (as arbitrary JSON data)
  • optional extra JSON attributes
Source code in jwskate/jws/signature.py
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 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
132
133
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
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
class JwsSignature(BaseJsonDict):
    """Represent a JWS Signature.

    A JWS Signature has:

     - a protected header (as a JSON object)
     - a signature value (as raw data)
     - an unprotected header (as arbitrary JSON data)
     - optional extra JSON attributes

    """

    @classmethod
    def from_parts(
        cls: type[S],
        protected: Mapping[str, Any],
        signature: bytes,
        header: Any | None,
        **kwargs: Any,
    ) -> S:
        """Initialize a JwsSignature based on the provided parts.

        Args:
          protected: the protected headers, as a key: value mapping
          signature: the raw signature value
          header: the unprotected header, if any
          **kwargs: extra attributes, if any

        Returns:
            A `JwsSignature` based on the provided parts.

        """
        content = dict(
            kwargs,
            protected=BinaPy.serialize_to("json", protected).to("b64u").ascii(),
            signature=BinaPy(signature).to("b64u").ascii(),
        )
        if header is not None:
            content["header"] = header
        return cls(content)

    @cached_property
    def protected(self) -> dict[str, Any]:
        """The protected header.

        Returns:
            the protected headers, as a `dict`.

        Raises:
            AttributeError: if this signature doesn't have protected headers.

        """
        protected = self.get("protected")
        if protected is None:
            msg = "This Jws JSON does not contain a 'protected' member"
            raise AttributeError(msg)
        return BinaPy(protected).decode_from("b64u").parse_from("json")  # type: ignore[no-any-return]

    @property
    def header(self) -> Any:
        """The unprotected header, unaltered.

        Returns:
            The unprotected header

        """
        return self.get("header")

    @cached_property
    def signature(self) -> bytes:
        """The raw signature.

        Returns:
            The raw signed data, unencoded

        Raises:
            AttributeError: if no 'signature' member is present

        """
        signature = self.get("signature")
        if signature is None:
            msg = "This Jws JSON does not contain a 'signature' member"
            raise AttributeError(msg)
        return BinaPy(signature).decode_from("b64u")

    @classmethod
    def sign(
        cls: type[S],
        payload: bytes,
        key: Jwk | Mapping[str, Any] | Any,
        alg: str | None = None,
        extra_protected_headers: Mapping[str, Any] | None = None,
        header: Any | None = None,
        **kwargs: Any,
    ) -> S:
        """Sign a payload and return the generated JWS signature.

        Args:
          payload: the raw data to sign
          key: the signature key to use
          alg: the signature algorithm to use
          extra_protected_headers: additional protected headers to include, if any
          header: the unprotected header, if any.
          **kwargs: additional members to include in this signature

        Returns:
            The generated signature.

        """
        key = to_jwk(key)

        headers = dict(extra_protected_headers or {}, alg=alg)
        kid = key.get("kid")
        if kid:
            headers["kid"] = kid

        signed_part = JwsSignature.assemble_signed_part(headers, payload)
        signature = key.sign(signed_part, alg=alg)
        return cls.from_parts(protected=headers, signature=signature, header=header, **kwargs)

    @classmethod
    def assemble_signed_part(cls, headers: Mapping[str, Any], payload: bytes | str) -> bytes:
        """Assemble the protected header and payload to sign, as specified in.

        [RFC7515
        $5.1](https://datatracker.ietf.org/doc/html/rfc7515#section-5.1).

        Args:
          headers: the protected headers
          payload: the raw payload to sign

        Returns:
            the raw data to sign

        """
        return b".".join(
            (
                BinaPy.serialize_to("json", headers).to("b64u"),
                BinaPy(payload).to("b64u"),
            )
        )

    def verify(
        self,
        payload: bytes,
        key: Jwk | Mapping[str, Any] | Any,
        *,
        alg: str | None = None,
        algs: Iterable[str] | None = None,
    ) -> bool:
        """Verify this signature against the given payload using the provided key.

        Args:
          payload: the raw payload
          key: the validation key to use
          alg: the signature alg t if only 1 is allowed
          algs: the allowed signature algs, if there are several

        Returns:
            `True` if the signature is verifier, `False` otherwise

        """
        key = to_jwk(key)
        signed_part = self.assemble_signed_part(self.protected, payload)
        return key.verify(signed_part, self.signature, alg=alg, algs=algs)

protected cached property

1
protected: dict[str, Any]

The protected header.

Returns:

Type Description
dict[str, Any]

the protected headers, as a dict.

Raises:

Type Description
AttributeError

if this signature doesn't have protected headers.

header property

1
header: Any

The unprotected header, unaltered.

Returns:

Type Description
Any

The unprotected header

signature cached property

1
signature: bytes

The raw signature.

Returns:

Type Description
bytes

The raw signed data, unencoded

Raises:

Type Description
AttributeError

if no 'signature' member is present

from_parts classmethod

1
2
3
4
5
6
from_parts(
    protected: Mapping[str, Any],
    signature: bytes,
    header: Any | None,
    **kwargs: Any
) -> S

Initialize a JwsSignature based on the provided parts.

Parameters:

Name Type Description Default
protected Mapping[str, Any]

the protected headers, as a key: value mapping

required
signature bytes

the raw signature value

required
header Any | None

the unprotected header, if any

required
**kwargs Any

extra attributes, if any

{}

Returns:

Type Description
S

A JwsSignature based on the provided parts.

Source code in jwskate/jws/signature.py
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
@classmethod
def from_parts(
    cls: type[S],
    protected: Mapping[str, Any],
    signature: bytes,
    header: Any | None,
    **kwargs: Any,
) -> S:
    """Initialize a JwsSignature based on the provided parts.

    Args:
      protected: the protected headers, as a key: value mapping
      signature: the raw signature value
      header: the unprotected header, if any
      **kwargs: extra attributes, if any

    Returns:
        A `JwsSignature` based on the provided parts.

    """
    content = dict(
        kwargs,
        protected=BinaPy.serialize_to("json", protected).to("b64u").ascii(),
        signature=BinaPy(signature).to("b64u").ascii(),
    )
    if header is not None:
        content["header"] = header
    return cls(content)

sign classmethod

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
sign(
    payload: bytes,
    key: Jwk | Mapping[str, Any] | Any,
    alg: str | None = None,
    extra_protected_headers: (
        Mapping[str, Any] | None
    ) = None,
    header: Any | None = None,
    **kwargs: Any
) -> S

Sign a payload and return the generated JWS signature.

Parameters:

Name Type Description Default
payload bytes

the raw data to sign

required
key Jwk | Mapping[str, Any] | Any

the signature key to use

required
alg str | None

the signature algorithm to use

None
extra_protected_headers Mapping[str, Any] | None

additional protected headers to include, if any

None
header Any | None

the unprotected header, if any.

None
**kwargs Any

additional members to include in this signature

{}

Returns:

Type Description
S

The generated signature.

Source code in jwskate/jws/signature.py
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
@classmethod
def sign(
    cls: type[S],
    payload: bytes,
    key: Jwk | Mapping[str, Any] | Any,
    alg: str | None = None,
    extra_protected_headers: Mapping[str, Any] | None = None,
    header: Any | None = None,
    **kwargs: Any,
) -> S:
    """Sign a payload and return the generated JWS signature.

    Args:
      payload: the raw data to sign
      key: the signature key to use
      alg: the signature algorithm to use
      extra_protected_headers: additional protected headers to include, if any
      header: the unprotected header, if any.
      **kwargs: additional members to include in this signature

    Returns:
        The generated signature.

    """
    key = to_jwk(key)

    headers = dict(extra_protected_headers or {}, alg=alg)
    kid = key.get("kid")
    if kid:
        headers["kid"] = kid

    signed_part = JwsSignature.assemble_signed_part(headers, payload)
    signature = key.sign(signed_part, alg=alg)
    return cls.from_parts(protected=headers, signature=signature, header=header, **kwargs)

assemble_signed_part classmethod

1
2
3
assemble_signed_part(
    headers: Mapping[str, Any], payload: bytes | str
) -> bytes

Assemble the protected header and payload to sign, as specified in.

RFC7515 $5.1.

Parameters:

Name Type Description Default
headers Mapping[str, Any]

the protected headers

required
payload bytes | str

the raw payload to sign

required

Returns:

Type Description
bytes

the raw data to sign

Source code in jwskate/jws/signature.py
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
@classmethod
def assemble_signed_part(cls, headers: Mapping[str, Any], payload: bytes | str) -> bytes:
    """Assemble the protected header and payload to sign, as specified in.

    [RFC7515
    $5.1](https://datatracker.ietf.org/doc/html/rfc7515#section-5.1).

    Args:
      headers: the protected headers
      payload: the raw payload to sign

    Returns:
        the raw data to sign

    """
    return b".".join(
        (
            BinaPy.serialize_to("json", headers).to("b64u"),
            BinaPy(payload).to("b64u"),
        )
    )

verify

1
2
3
4
5
6
7
verify(
    payload: bytes,
    key: Jwk | Mapping[str, Any] | Any,
    *,
    alg: str | None = None,
    algs: Iterable[str] | None = None
) -> bool

Verify this signature against the given payload using the provided key.

Parameters:

Name Type Description Default
payload bytes

the raw payload

required
key Jwk | Mapping[str, Any] | Any

the validation key to use

required
alg str | None

the signature alg t if only 1 is allowed

None
algs Iterable[str] | None

the allowed signature algs, if there are several

None

Returns:

Type Description
bool

True if the signature is verifier, False otherwise

Source code in jwskate/jws/signature.py
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
def verify(
    self,
    payload: bytes,
    key: Jwk | Mapping[str, Any] | Any,
    *,
    alg: str | None = None,
    algs: Iterable[str] | None = None,
) -> bool:
    """Verify this signature against the given payload using the provided key.

    Args:
      payload: the raw payload
      key: the validation key to use
      alg: the signature alg t if only 1 is allowed
      algs: the allowed signature algs, if there are several

    Returns:
        `True` if the signature is verifier, `False` otherwise

    """
    key = to_jwk(key)
    signed_part = self.assemble_signed_part(self.protected, payload)
    return key.verify(signed_part, self.signature, alg=alg, algs=algs)

jwskate.jwe

This module implements Json Web Encryption as described in [RFC7516].

[RFC7516] : https: //www.rfc-editor.org/rfc/rfc7516

InvalidJwe

Bases: ValueError

Raised when an invalid JWE token is parsed.

Source code in jwskate/jwe/compact.py
25
26
class InvalidJwe(ValueError):
    """Raised when an invalid JWE token is parsed."""

JweCompact

Bases: BaseCompactToken

Represents a Json Web Encryption object, in compact representation, as defined in RFC7516.

Parameters:

Name Type Description Default
value bytes | str

the compact representation for this Jwe

required
Source code in jwskate/jwe/compact.py
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 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
132
133
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
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
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
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
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
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
class JweCompact(BaseCompactToken):
    """Represents a Json Web Encryption object, in compact representation, as defined in RFC7516.

    Args:
        value: the compact representation for this Jwe

    """

    def __init__(self, value: bytes | str, max_size: int = 16 * 1024):
        super().__init__(value, max_size)

        parts = BinaPy(self.value).split(b".")
        if len(parts) != 5:  # noqa: PLR2004
            msg = """Invalid JWE: a JWE must contain:
    - a header,
    - an encrypted key,
    - an IV,
    - a ciphertext
    - an authentication tag
separated by dots."""
            raise InvalidJwe(msg)

        header, cek, iv, ciphertext, auth_tag = parts
        try:
            headers = header.decode_from("b64u").parse_from("json")
        except ValueError as exc:
            msg = "Invalid JWE header: it must be a Base64URL-encoded JSON object."
            raise InvalidJwe(msg) from exc
        enc = headers.get("enc")
        if enc is None or not isinstance(enc, str):
            msg = "Invalid JWE header: this JWE doesn't have a valid 'enc' header."
            raise InvalidJwe(msg)
        self.headers = headers
        self.additional_authenticated_data = header

        try:
            self.wrapped_cek = cek.decode_from("b64u")
        except ValueError as exc:
            msg = "Invalid JWE CEK: it must be a Base64URL-encoded binary data."
            raise InvalidJwe(msg) from exc

        try:
            self.initialization_vector = iv.decode_from("b64u")
        except ValueError as exc:
            msg = "Invalid JWE IV: it must be a Base64URL-encoded binary data."
            raise InvalidJwe(msg) from exc

        try:
            self.ciphertext = ciphertext.decode_from("b64u")
        except ValueError as exc:
            msg = "Invalid JWE ciphertext: it must be a Base64URL-encoded binary data."
            raise InvalidJwe(msg) from exc

        try:
            self.authentication_tag = BinaPy(auth_tag).decode_from("b64u")
        except ValueError as exc:
            msg = "Invalid JWE authentication tag: it must be a Base64URL-encoded binary data."
            raise InvalidJwe(msg) from exc

    @classmethod
    def from_parts(
        cls,
        *,
        headers: Mapping[str, Any],
        cek: bytes,
        iv: bytes,
        ciphertext: bytes,
        tag: bytes,
    ) -> JweCompact:
        """Initialize a `JweCompact` from its different parts (header, cek, iv, ciphertext, tag).

        Args:
          headers: the headers (as a mapping of name: value)
          cek: the raw CEK
          iv: the raw IV
          ciphertext: the raw ciphertext
          tag: the authentication tag

        Returns:
            the initialized `JweCompact` instance

        """
        return cls(
            b".".join(
                (
                    BinaPy.serialize_to("json", headers).to("b64u"),
                    BinaPy(cek).to("b64u"),
                    BinaPy(iv).to("b64u"),
                    BinaPy(ciphertext).to("b64u"),
                    BinaPy(tag).to("b64u"),
                )
            )
        )

    @cached_property
    def enc(self) -> str:
        """Return the `enc` from the JWE header.

        The `enc` header contains the identifier of the CEK encryption algorithm.

        Returns:
            the enc value

        Raises:
            AttributeError: if there is no enc header or it is not a string

        """
        return self.get_header("enc")  # type: ignore[no-any-return]
        # header has been checked at init time

    @classmethod
    def encrypt(
        cls,
        plaintext: bytes | SupportsBytes,
        key: Jwk | Mapping[str, Any] | Any,
        *,
        enc: str,
        alg: str | None = None,
        extra_headers: Mapping[str, Any] | None = None,
        cek: bytes | None = None,
        iv: bytes | None = None,
        epk: Jwk | None = None,
    ) -> JweCompact:
        """Encrypt an arbitrary plaintext into a `JweCompact`.

        Args:
          plaintext: the raw plaintext to encrypt
          key: the public or symmetric key to use for encryption
          enc: the encryption algorithm to use
          alg: the Key Management algorithm to use, if there is no 'alg' header defined in the `Jwk`
          extra_headers: additional headers to include in the generated token
          cek: the CEK to force use, for algorithms relying on a random CEK.
              Leave `None` to have a safe value generated automatically.
          iv: the IV to force use. Leave `None` to have a safe value generated automatically.
          epk: the EPK to force use. Leave `None` to have a safe value generated automatically.

        Returns:
            the generated JweCompact instance

        """
        extra_headers = extra_headers or {}
        key = to_jwk(key)
        alg = select_alg_class(key.KEY_MANAGEMENT_ALGORITHMS, jwk_alg=key.alg, alg=alg).name

        cek_jwk, wrapped_cek, cek_headers = key.sender_key(enc=enc, alg=alg, cek=cek, epk=epk, **extra_headers)

        headers = dict(extra_headers, **cek_headers, alg=alg, enc=enc)
        if key.kid is not None:
            headers["kid"] = key.kid

        aad = BinaPy.serialize_to("json", headers).to("b64u")

        ciphertext, iv, tag = cek_jwk.encrypt(plaintext, aad=aad, iv=iv, alg=enc)

        return cls.from_parts(headers=headers, cek=wrapped_cek, iv=iv, ciphertext=ciphertext, tag=tag)

    PBES2_ALGORITHMS: Mapping[str, type[BasePbes2]] = {
        alg.name: alg for alg in [Pbes2_HS256_A128KW, Pbes2_HS384_A192KW, Pbes2_HS512_A256KW]
    }

    def unwrap_cek(
        self,
        key_or_password: Jwk | Mapping[str, Any] | bytes | str,
        alg: str | None = None,
        algs: Iterable[str] | None = None,
    ) -> Jwk:
        """Unwrap the CEK from this `Jwe` using the provided key or password.

        Args:
          key_or_password: the decryption JWK or password
          alg: allowed key management algorithm, if there is only 1
          algs: allowed key managements algorithms, if there are several

        Returns:
            the unwrapped CEK, as a SymmetricJwk

        """
        if isinstance(key_or_password, (bytes, str)):
            password = key_or_password
            return self.unwrap_cek_with_password(password)

        jwk = to_jwk(key_or_password)
        select_alg_classes(
            jwk.KEY_MANAGEMENT_ALGORITHMS,
            jwk_alg=self.alg,
            alg=alg,
            algs=algs,
            strict=True,
        )
        cek = jwk.recipient_key(self.wrapped_cek, **self.headers)
        return cek

    def decrypt(
        self,
        key: Jwk | Mapping[str, Any] | Any,
        *,
        alg: str | None = None,
        algs: Iterable[str] | None = None,
    ) -> BinaPy:
        """Decrypt the payload from this JWE using a decryption key.

        Args:
          key: the decryption key
          alg: allowed key management algorithm, if there is only 1
          algs: allowed keys management algorithms, if there are several

        Returns:
          the decrypted payload

        """
        cek_jwk = self.unwrap_cek(key, alg=alg, algs=algs)

        plaintext = cek_jwk.decrypt(
            ciphertext=self.ciphertext,
            iv=self.initialization_vector,
            tag=self.authentication_tag,
            aad=self.additional_authenticated_data,
            alg=self.enc,
        )
        return plaintext

    def decrypt_jwt(
        self,
        key: Jwk | Mapping[str, Any] | Any,
        *,
        alg: str | None = None,
        algs: Iterable[str] | None = None,
    ) -> SignedJwt:
        """Convenience method to decrypt an inner JWT.

        Takes the same args as decrypt(), but returns a `SignedJwt`.

        Raises:
            InvalidJwt: if the content is not a syntactically valid signed JWT.

        """
        from jwskate.jwt import SignedJwt

        raw = self.decrypt(key, alg=alg, algs=algs)
        return SignedJwt(raw)

    @classmethod
    def encrypt_with_password(
        cls,
        plaintext: SupportsBytes | bytes,
        password: SupportsBytes | bytes | str,
        *,
        alg: str,
        enc: str,
        salt: bytes | None = None,
        count: int = 2000,
        cek: bytes | None = None,
        iv: bytes | None = None,
    ) -> JweCompact:
        """Encrypt a payload with a password and return the resulting JweCompact.

        This performs symmetric encryption using PBES2.

        Args:
          plaintext: the data to encrypt
          password: the password to use
          alg: the Key Management alg to use
          enc: the Payload Encryption alg to use
          salt: the salt to use. Leave `None` (default) to have `jwskate` generate a safe random value
          count: the number of PBES2 iterations (recommended minimum 1000)
          cek: the CEK to force use. Leave `None` (default) to have `jwskate` generate a safe random value
          iv: the IV to force use. Leave `None` (default) to have `jwskate` generate a safe random value

        Returns:
            the resulting JweCompact

        Raises:
            UnsupportedAlg: if the key management alg is not supported
            ValueError: if the `count` parameter is not a positive integer

        """
        keyalg = cls.PBES2_ALGORITHMS.get(alg)
        if keyalg is None:
            msg = (
                f"Unsupported password-based encryption algorithm '{alg}'. "
                "Value must be one of {list(cls.PBES2_ALGORITHMS.keys())}."
            )
            raise UnsupportedAlg(msg)

        if cek is None:
            cek_jwk = SymmetricJwk.generate_for_alg(enc)
            cek = cek_jwk.key
        else:
            cek_jwk = SymmetricJwk.from_bytes(cek)

        wrapper = keyalg(password)
        if salt is None:
            salt = wrapper.generate_salt()

        if count < 1:
            msg = "PBES2 iteration count must be a positive integer, with a minimum recommended value of 1000."
            raise ValueError(msg)
        if count < 1000:  # noqa: PLR2004
            warnings.warn("PBES2 iteration count should be > 1000.", stacklevel=2)

        wrapped_cek = wrapper.wrap_key(cek, salt=salt, count=count)

        headers = {"alg": alg, "enc": enc, "p2s": BinaPy(salt).to("b64u").ascii(), "p2c": count}
        aad = BinaPy.serialize_to("json", headers).to("b64u")
        ciphertext, iv, tag = cek_jwk.encrypt(plaintext=plaintext, aad=aad, alg=enc, iv=iv)

        return cls.from_parts(headers=headers, cek=wrapped_cek, iv=iv, ciphertext=ciphertext, tag=tag)

    def unwrap_cek_with_password(self, password: bytes | str) -> Jwk:
        """Unwrap a CEK using a password. Works only for password-encrypted JWE Tokens.

        Args:
          password: the decryption password

        Returns:
            the CEK, as a SymmetricJwk instance

        Raises:
            UnsupportedAlg: if the token key management algorithm is not supported
            AttributeError: if the token misses the PBES2-related headers

        """
        keyalg = self.PBES2_ALGORITHMS.get(self.alg)
        if keyalg is None:
            msg = (
                f"Unsupported password-based encryption algorithm '{self.alg}'. "
                "Value must be one of {list(self.PBES2_ALGORITHMS.keys())}."
            )
            raise UnsupportedAlg(msg)
        p2s = self.headers.get("p2s")
        if p2s is None:
            msg = "Invalid JWE: a required 'p2s' header is missing."
            raise InvalidJwe(msg)
        salt = BinaPy(p2s).decode_from("b64u")
        p2c = self.headers.get("p2c")
        if p2c is None:
            msg = "Invalid JWE: a required 'p2c' header is missing."
            raise InvalidJwe(msg)
        if not isinstance(p2c, int) or p2c < 1:
            msg = "Invalid JWE: invalid value for the 'p2c' header, must be a positive integer."
            raise InvalidJwe(msg)
        wrapper = keyalg(password)
        cek = wrapper.unwrap_key(self.wrapped_cek, salt=salt, count=p2c)
        return SymmetricJwk.from_bytes(cek)

    def decrypt_with_password(self, password: bytes | str) -> bytes:
        """Decrypt this JWE with a password.

        This only works for tokens encrypted with a password.

        Args:
          password: the password to use

        Returns:
            the unencrypted payload

        """
        cek_jwk = self.unwrap_cek_with_password(password)
        plaintext = cek_jwk.decrypt(
            ciphertext=self.ciphertext,
            iv=self.initialization_vector,
            tag=self.authentication_tag,
            aad=self.additional_authenticated_data,
            alg=self.enc,
        )
        return plaintext

enc cached property

1
enc: str

Return the enc from the JWE header.

The enc header contains the identifier of the CEK encryption algorithm.

Returns:

Type Description
str

the enc value

Raises:

Type Description
AttributeError

if there is no enc header or it is not a string

from_parts classmethod

1
2
3
4
5
6
7
8
from_parts(
    *,
    headers: Mapping[str, Any],
    cek: bytes,
    iv: bytes,
    ciphertext: bytes,
    tag: bytes
) -> JweCompact

Initialize a JweCompact from its different parts (header, cek, iv, ciphertext, tag).

Parameters:

Name Type Description Default
headers Mapping[str, Any]

the headers (as a mapping of name: value)

required
cek bytes

the raw CEK

required
iv bytes

the raw IV

required
ciphertext bytes

the raw ciphertext

required
tag bytes

the authentication tag

required

Returns:

Type Description
JweCompact

the initialized JweCompact instance

Source code in jwskate/jwe/compact.py
 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
@classmethod
def from_parts(
    cls,
    *,
    headers: Mapping[str, Any],
    cek: bytes,
    iv: bytes,
    ciphertext: bytes,
    tag: bytes,
) -> JweCompact:
    """Initialize a `JweCompact` from its different parts (header, cek, iv, ciphertext, tag).

    Args:
      headers: the headers (as a mapping of name: value)
      cek: the raw CEK
      iv: the raw IV
      ciphertext: the raw ciphertext
      tag: the authentication tag

    Returns:
        the initialized `JweCompact` instance

    """
    return cls(
        b".".join(
            (
                BinaPy.serialize_to("json", headers).to("b64u"),
                BinaPy(cek).to("b64u"),
                BinaPy(iv).to("b64u"),
                BinaPy(ciphertext).to("b64u"),
                BinaPy(tag).to("b64u"),
            )
        )
    )

encrypt classmethod

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
encrypt(
    plaintext: bytes | SupportsBytes,
    key: Jwk | Mapping[str, Any] | Any,
    *,
    enc: str,
    alg: str | None = None,
    extra_headers: Mapping[str, Any] | None = None,
    cek: bytes | None = None,
    iv: bytes | None = None,
    epk: Jwk | None = None
) -> JweCompact

Encrypt an arbitrary plaintext into a JweCompact.

Parameters:

Name Type Description Default
plaintext bytes | SupportsBytes

the raw plaintext to encrypt

required
key Jwk | Mapping[str, Any] | Any

the public or symmetric key to use for encryption

required
enc str

the encryption algorithm to use

required
alg str | None

the Key Management algorithm to use, if there is no 'alg' header defined in the Jwk

None
extra_headers Mapping[str, Any] | None

additional headers to include in the generated token

None
cek bytes | None

the CEK to force use, for algorithms relying on a random CEK. Leave None to have a safe value generated automatically.

None
iv bytes | None

the IV to force use. Leave None to have a safe value generated automatically.

None
epk Jwk | None

the EPK to force use. Leave None to have a safe value generated automatically.

None

Returns:

Type Description
JweCompact

the generated JweCompact instance

Source code in jwskate/jwe/compact.py
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
173
174
175
176
177
178
179
180
181
182
183
@classmethod
def encrypt(
    cls,
    plaintext: bytes | SupportsBytes,
    key: Jwk | Mapping[str, Any] | Any,
    *,
    enc: str,
    alg: str | None = None,
    extra_headers: Mapping[str, Any] | None = None,
    cek: bytes | None = None,
    iv: bytes | None = None,
    epk: Jwk | None = None,
) -> JweCompact:
    """Encrypt an arbitrary plaintext into a `JweCompact`.

    Args:
      plaintext: the raw plaintext to encrypt
      key: the public or symmetric key to use for encryption
      enc: the encryption algorithm to use
      alg: the Key Management algorithm to use, if there is no 'alg' header defined in the `Jwk`
      extra_headers: additional headers to include in the generated token
      cek: the CEK to force use, for algorithms relying on a random CEK.
          Leave `None` to have a safe value generated automatically.
      iv: the IV to force use. Leave `None` to have a safe value generated automatically.
      epk: the EPK to force use. Leave `None` to have a safe value generated automatically.

    Returns:
        the generated JweCompact instance

    """
    extra_headers = extra_headers or {}
    key = to_jwk(key)
    alg = select_alg_class(key.KEY_MANAGEMENT_ALGORITHMS, jwk_alg=key.alg, alg=alg).name

    cek_jwk, wrapped_cek, cek_headers = key.sender_key(enc=enc, alg=alg, cek=cek, epk=epk, **extra_headers)

    headers = dict(extra_headers, **cek_headers, alg=alg, enc=enc)
    if key.kid is not None:
        headers["kid"] = key.kid

    aad = BinaPy.serialize_to("json", headers).to("b64u")

    ciphertext, iv, tag = cek_jwk.encrypt(plaintext, aad=aad, iv=iv, alg=enc)

    return cls.from_parts(headers=headers, cek=wrapped_cek, iv=iv, ciphertext=ciphertext, tag=tag)

unwrap_cek

1
2
3
4
5
unwrap_cek(
    key_or_password: Jwk | Mapping[str, Any] | bytes | str,
    alg: str | None = None,
    algs: Iterable[str] | None = None,
) -> Jwk

Unwrap the CEK from this Jwe using the provided key or password.

Parameters:

Name Type Description Default
key_or_password Jwk | Mapping[str, Any] | bytes | str

the decryption JWK or password

required
alg str | None

allowed key management algorithm, if there is only 1

None
algs Iterable[str] | None

allowed key managements algorithms, if there are several

None

Returns:

Type Description
Jwk

the unwrapped CEK, as a SymmetricJwk

Source code in jwskate/jwe/compact.py
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
def unwrap_cek(
    self,
    key_or_password: Jwk | Mapping[str, Any] | bytes | str,
    alg: str | None = None,
    algs: Iterable[str] | None = None,
) -> Jwk:
    """Unwrap the CEK from this `Jwe` using the provided key or password.

    Args:
      key_or_password: the decryption JWK or password
      alg: allowed key management algorithm, if there is only 1
      algs: allowed key managements algorithms, if there are several

    Returns:
        the unwrapped CEK, as a SymmetricJwk

    """
    if isinstance(key_or_password, (bytes, str)):
        password = key_or_password
        return self.unwrap_cek_with_password(password)

    jwk = to_jwk(key_or_password)
    select_alg_classes(
        jwk.KEY_MANAGEMENT_ALGORITHMS,
        jwk_alg=self.alg,
        alg=alg,
        algs=algs,
        strict=True,
    )
    cek = jwk.recipient_key(self.wrapped_cek, **self.headers)
    return cek

decrypt

1
2
3
4
5
6
decrypt(
    key: Jwk | Mapping[str, Any] | Any,
    *,
    alg: str | None = None,
    algs: Iterable[str] | None = None
) -> BinaPy

Decrypt the payload from this JWE using a decryption key.

Parameters:

Name Type Description Default
key Jwk | Mapping[str, Any] | Any

the decryption key

required
alg str | None

allowed key management algorithm, if there is only 1

None
algs Iterable[str] | None

allowed keys management algorithms, if there are several

None

Returns:

Type Description
BinaPy

the decrypted payload

Source code in jwskate/jwe/compact.py
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
def decrypt(
    self,
    key: Jwk | Mapping[str, Any] | Any,
    *,
    alg: str | None = None,
    algs: Iterable[str] | None = None,
) -> BinaPy:
    """Decrypt the payload from this JWE using a decryption key.

    Args:
      key: the decryption key
      alg: allowed key management algorithm, if there is only 1
      algs: allowed keys management algorithms, if there are several

    Returns:
      the decrypted payload

    """
    cek_jwk = self.unwrap_cek(key, alg=alg, algs=algs)

    plaintext = cek_jwk.decrypt(
        ciphertext=self.ciphertext,
        iv=self.initialization_vector,
        tag=self.authentication_tag,
        aad=self.additional_authenticated_data,
        alg=self.enc,
    )
    return plaintext

decrypt_jwt

1
2
3
4
5
6
decrypt_jwt(
    key: Jwk | Mapping[str, Any] | Any,
    *,
    alg: str | None = None,
    algs: Iterable[str] | None = None
) -> SignedJwt

Convenience method to decrypt an inner JWT.

Takes the same args as decrypt(), but returns a SignedJwt.

Raises:

Type Description
InvalidJwt

if the content is not a syntactically valid signed JWT.

Source code in jwskate/jwe/compact.py
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
def decrypt_jwt(
    self,
    key: Jwk | Mapping[str, Any] | Any,
    *,
    alg: str | None = None,
    algs: Iterable[str] | None = None,
) -> SignedJwt:
    """Convenience method to decrypt an inner JWT.

    Takes the same args as decrypt(), but returns a `SignedJwt`.

    Raises:
        InvalidJwt: if the content is not a syntactically valid signed JWT.

    """
    from jwskate.jwt import SignedJwt

    raw = self.decrypt(key, alg=alg, algs=algs)
    return SignedJwt(raw)

encrypt_with_password classmethod

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
encrypt_with_password(
    plaintext: SupportsBytes | bytes,
    password: SupportsBytes | bytes | str,
    *,
    alg: str,
    enc: str,
    salt: bytes | None = None,
    count: int = 2000,
    cek: bytes | None = None,
    iv: bytes | None = None
) -> JweCompact

Encrypt a payload with a password and return the resulting JweCompact.

This performs symmetric encryption using PBES2.

Parameters:

Name Type Description Default
plaintext SupportsBytes | bytes

the data to encrypt

required
password SupportsBytes | bytes | str

the password to use

required
alg str

the Key Management alg to use

required
enc str

the Payload Encryption alg to use

required
salt bytes | None

the salt to use. Leave None (default) to have jwskate generate a safe random value

None
count int

the number of PBES2 iterations (recommended minimum 1000)

2000
cek bytes | None

the CEK to force use. Leave None (default) to have jwskate generate a safe random value

None
iv bytes | None

the IV to force use. Leave None (default) to have jwskate generate a safe random value

None

Returns:

Type Description
JweCompact

the resulting JweCompact

Raises:

Type Description
UnsupportedAlg

if the key management alg is not supported

ValueError

if the count parameter is not a positive integer

Source code in jwskate/jwe/compact.py
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
@classmethod
def encrypt_with_password(
    cls,
    plaintext: SupportsBytes | bytes,
    password: SupportsBytes | bytes | str,
    *,
    alg: str,
    enc: str,
    salt: bytes | None = None,
    count: int = 2000,
    cek: bytes | None = None,
    iv: bytes | None = None,
) -> JweCompact:
    """Encrypt a payload with a password and return the resulting JweCompact.

    This performs symmetric encryption using PBES2.

    Args:
      plaintext: the data to encrypt
      password: the password to use
      alg: the Key Management alg to use
      enc: the Payload Encryption alg to use
      salt: the salt to use. Leave `None` (default) to have `jwskate` generate a safe random value
      count: the number of PBES2 iterations (recommended minimum 1000)
      cek: the CEK to force use. Leave `None` (default) to have `jwskate` generate a safe random value
      iv: the IV to force use. Leave `None` (default) to have `jwskate` generate a safe random value

    Returns:
        the resulting JweCompact

    Raises:
        UnsupportedAlg: if the key management alg is not supported
        ValueError: if the `count` parameter is not a positive integer

    """
    keyalg = cls.PBES2_ALGORITHMS.get(alg)
    if keyalg is None:
        msg = (
            f"Unsupported password-based encryption algorithm '{alg}'. "
            "Value must be one of {list(cls.PBES2_ALGORITHMS.keys())}."
        )
        raise UnsupportedAlg(msg)

    if cek is None:
        cek_jwk = SymmetricJwk.generate_for_alg(enc)
        cek = cek_jwk.key
    else:
        cek_jwk = SymmetricJwk.from_bytes(cek)

    wrapper = keyalg(password)
    if salt is None:
        salt = wrapper.generate_salt()

    if count < 1:
        msg = "PBES2 iteration count must be a positive integer, with a minimum recommended value of 1000."
        raise ValueError(msg)
    if count < 1000:  # noqa: PLR2004
        warnings.warn("PBES2 iteration count should be > 1000.", stacklevel=2)

    wrapped_cek = wrapper.wrap_key(cek, salt=salt, count=count)

    headers = {"alg": alg, "enc": enc, "p2s": BinaPy(salt).to("b64u").ascii(), "p2c": count}
    aad = BinaPy.serialize_to("json", headers).to("b64u")
    ciphertext, iv, tag = cek_jwk.encrypt(plaintext=plaintext, aad=aad, alg=enc, iv=iv)

    return cls.from_parts(headers=headers, cek=wrapped_cek, iv=iv, ciphertext=ciphertext, tag=tag)

unwrap_cek_with_password

1
unwrap_cek_with_password(password: bytes | str) -> Jwk

Unwrap a CEK using a password. Works only for password-encrypted JWE Tokens.

Parameters:

Name Type Description Default
password bytes | str

the decryption password

required

Returns:

Type Description
Jwk

the CEK, as a SymmetricJwk instance

Raises:

Type Description
UnsupportedAlg

if the token key management algorithm is not supported

AttributeError

if the token misses the PBES2-related headers

Source code in jwskate/jwe/compact.py
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
def unwrap_cek_with_password(self, password: bytes | str) -> Jwk:
    """Unwrap a CEK using a password. Works only for password-encrypted JWE Tokens.

    Args:
      password: the decryption password

    Returns:
        the CEK, as a SymmetricJwk instance

    Raises:
        UnsupportedAlg: if the token key management algorithm is not supported
        AttributeError: if the token misses the PBES2-related headers

    """
    keyalg = self.PBES2_ALGORITHMS.get(self.alg)
    if keyalg is None:
        msg = (
            f"Unsupported password-based encryption algorithm '{self.alg}'. "
            "Value must be one of {list(self.PBES2_ALGORITHMS.keys())}."
        )
        raise UnsupportedAlg(msg)
    p2s = self.headers.get("p2s")
    if p2s is None:
        msg = "Invalid JWE: a required 'p2s' header is missing."
        raise InvalidJwe(msg)
    salt = BinaPy(p2s).decode_from("b64u")
    p2c = self.headers.get("p2c")
    if p2c is None:
        msg = "Invalid JWE: a required 'p2c' header is missing."
        raise InvalidJwe(msg)
    if not isinstance(p2c, int) or p2c < 1:
        msg = "Invalid JWE: invalid value for the 'p2c' header, must be a positive integer."
        raise InvalidJwe(msg)
    wrapper = keyalg(password)
    cek = wrapper.unwrap_key(self.wrapped_cek, salt=salt, count=p2c)
    return SymmetricJwk.from_bytes(cek)

decrypt_with_password

1
decrypt_with_password(password: bytes | str) -> bytes

Decrypt this JWE with a password.

This only works for tokens encrypted with a password.

Parameters:

Name Type Description Default
password bytes | str

the password to use

required

Returns:

Type Description
bytes

the unencrypted payload

Source code in jwskate/jwe/compact.py
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
def decrypt_with_password(self, password: bytes | str) -> bytes:
    """Decrypt this JWE with a password.

    This only works for tokens encrypted with a password.

    Args:
      password: the password to use

    Returns:
        the unencrypted payload

    """
    cek_jwk = self.unwrap_cek_with_password(password)
    plaintext = cek_jwk.decrypt(
        ciphertext=self.ciphertext,
        iv=self.initialization_vector,
        tag=self.authentication_tag,
        aad=self.additional_authenticated_data,
        alg=self.enc,
    )
    return plaintext

jwskate.jwa

This module implements the Json Web Algorithms as defined in RFC7518.

Each algorithm is represented as a wrapper around a symmetric or asymmetric key, and exposes the cryptographic operations as methods. The cryptographic operations themselves are delegated to cryptography.

P_256 module-attribute

1
2
3
4
5
P_256: EllipticCurve = EllipticCurve(
    name="P-256",
    cryptography_curve=SECP256R1(),
    coordinate_size=32,
)

P-256 curve.

P_384 module-attribute

1
2
3
4
5
P_384: EllipticCurve = EllipticCurve(
    name="P-384",
    cryptography_curve=SECP384R1(),
    coordinate_size=48,
)

P-384 curve.

P_521 module-attribute

1
2
3
4
5
P_521: EllipticCurve = EllipticCurve(
    name="P-521",
    cryptography_curve=SECP521R1(),
    coordinate_size=66,
)

P-521 curve.

secp256k1 module-attribute

1
2
3
4
5
secp256k1: EllipticCurve = EllipticCurve(
    name="secp256k1",
    cryptography_curve=SECP256K1(),
    coordinate_size=32,
)

X448 module-attribute

1
2
3
4
5
6
7
8
X448 = OKPCurve(
    name="X448",
    description="X448 function key pairs",
    cryptography_private_key_class=X448PrivateKey,
    cryptography_public_key_class=X448PublicKey,
    use="enc",
    key_size=56,
)

X448 curve.

X25519 module-attribute

1
2
3
4
5
6
7
8
X25519 = OKPCurve(
    name="X25519",
    description="X25519 function key pairs",
    cryptography_private_key_class=X25519PrivateKey,
    cryptography_public_key_class=X25519PublicKey,
    use="enc",
    key_size=32,
)

X25519 curve.

Ed448 module-attribute

1
2
3
4
5
6
7
8
Ed448 = OKPCurve(
    name="Ed448",
    description="Ed448 signature algorithm key pairs",
    cryptography_private_key_class=Ed448PrivateKey,
    cryptography_public_key_class=Ed448PublicKey,
    use="sig",
    key_size=57,
)

Ed448 curve.

Ed25519 module-attribute

1
2
3
4
5
6
7
8
Ed25519 = OKPCurve(
    name="Ed25519",
    description="Ed25519 signature algorithm key pairs",
    cryptography_private_key_class=Ed25519PrivateKey,
    cryptography_public_key_class=Ed25519PublicKey,
    use="sig",
    key_size=32,
)

Ed25519 curve.

BaseAESEncryptionAlg

Bases: BaseSymmetricAlg

Base class for AES encryption algorithms.

Source code in jwskate/jwa/base.py
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
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
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
class BaseAESEncryptionAlg(BaseSymmetricAlg):
    """Base class for AES encryption algorithms."""

    use = "enc"

    key_size: int
    tag_size: int
    iv_size: int

    @classmethod
    def check_key(cls, key: bytes) -> None:
        """Check that a key is suitable for this algorithm.

        Args:
          key: the key to check

        Raises:
            InvalidKey: if the key is not suitable

        """
        if len(key) * 8 != cls.key_size:
            msg = f"This key size of {len(key) * 8} bits doesn't match the expected key size of {cls.key_size} bits"
            raise InvalidKey(msg)

    @classmethod
    def generate_key(cls) -> BinaPy:
        """Generate a key of an appropriate size for this AES alg subclass.

        Returns:
            a random AES key

        """
        return BinaPy.random_bits(cls.key_size)

    @classmethod
    def generate_iv(cls) -> BinaPy:
        """Generate an Initialisation Vector of the appropriate size.

        Returns:
            a random IV

        """
        return BinaPy.random_bits(cls.iv_size)

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

        This needs as parameters:

        - the raw data to encrypt (`plaintext`)
        - a given random Initialisation Vector (`iv`) of the appropriate size
        - optional Additional Authentication Data (`aad`)

        And returns a tuple `(ciphered_data, authentication_tag)`.

        Args:
          plaintext: the data to encrypt
          iv: the Initialisation Vector to use
          aad: the Additional Authentication Data

        Returns:
          a tuple of ciphered data and authentication tag

        """
        raise NotImplementedError

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

        This needs:

        - the raw encrypted Data (`ciphertext`) and Authentication Tag (`auth_tag`) that were produced by encryption,
        - the same Initialisation Vector (`iv`) and optional Additional Authentication Data that were provided for
        encryption.

        Returns the resulting clear text data.

        Args:
          ciphertext: the data to decrypt
          iv: the Initialisation Vector to use. Must be the same one used during encryption
          auth_tag: the authentication tag
          aad: the Additional Authentication Data. Must be the same one used during encryption

        Returns:
          the deciphered data

        """
        raise NotImplementedError

    @classmethod
    @override
    def with_random_key(cls) -> Self:
        """Initialize this alg with a random key.

        Returns:
            a subclass of `BaseAESEncryptionAlg` initialized with a randomly generated key

        """
        return cls(cls.generate_key())

check_key classmethod

1
check_key(key: bytes) -> None

Check that a key is suitable for this algorithm.

Parameters:

Name Type Description Default
key bytes

the key to check

required

Raises:

Type Description
InvalidKey

if the key is not suitable

Source code in jwskate/jwa/base.py
230
231
232
233
234
235
236
237
238
239
240
241
242
243
@classmethod
def check_key(cls, key: bytes) -> None:
    """Check that a key is suitable for this algorithm.

    Args:
      key: the key to check

    Raises:
        InvalidKey: if the key is not suitable

    """
    if len(key) * 8 != cls.key_size:
        msg = f"This key size of {len(key) * 8} bits doesn't match the expected key size of {cls.key_size} bits"
        raise InvalidKey(msg)

generate_key classmethod

1
generate_key() -> BinaPy

Generate a key of an appropriate size for this AES alg subclass.

Returns:

Type Description
BinaPy

a random AES key

Source code in jwskate/jwa/base.py
245
246
247
248
249
250
251
252
253
@classmethod
def generate_key(cls) -> BinaPy:
    """Generate a key of an appropriate size for this AES alg subclass.

    Returns:
        a random AES key

    """
    return BinaPy.random_bits(cls.key_size)

generate_iv classmethod

1
generate_iv() -> BinaPy

Generate an Initialisation Vector of the appropriate size.

Returns:

Type Description
BinaPy

a random IV

Source code in jwskate/jwa/base.py
255
256
257
258
259
260
261
262
263
@classmethod
def generate_iv(cls) -> BinaPy:
    """Generate an Initialisation Vector of the appropriate size.

    Returns:
        a random IV

    """
    return BinaPy.random_bits(cls.iv_size)

encrypt

1
2
3
4
5
6
encrypt(
    plaintext: bytes | SupportsBytes,
    *,
    iv: bytes | SupportsBytes,
    aad: bytes | SupportsBytes | None = None
) -> tuple[BinaPy, BinaPy]

Encrypt arbitrary data, with optional Authenticated Encryption.

This needs as parameters:

  • the raw data to encrypt (plaintext)
  • a given random Initialisation Vector (iv) of the appropriate size
  • optional Additional Authentication Data (aad)

And returns a tuple (ciphered_data, authentication_tag).

Parameters:

Name Type Description Default
plaintext bytes | SupportsBytes

the data to encrypt

required
iv bytes | SupportsBytes

the Initialisation Vector to use

required
aad bytes | SupportsBytes | None

the Additional Authentication Data

None

Returns:

Type Description
tuple[BinaPy, BinaPy]

a tuple of ciphered data and authentication tag

Source code in jwskate/jwa/base.py
265
266
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
def encrypt(
    self,
    plaintext: bytes | SupportsBytes,
    *,
    iv: bytes | SupportsBytes,
    aad: bytes | SupportsBytes | None = None,
) -> tuple[BinaPy, BinaPy]:
    """Encrypt arbitrary data, with optional Authenticated Encryption.

    This needs as parameters:

    - the raw data to encrypt (`plaintext`)
    - a given random Initialisation Vector (`iv`) of the appropriate size
    - optional Additional Authentication Data (`aad`)

    And returns a tuple `(ciphered_data, authentication_tag)`.

    Args:
      plaintext: the data to encrypt
      iv: the Initialisation Vector to use
      aad: the Additional Authentication Data

    Returns:
      a tuple of ciphered data and authentication tag

    """
    raise NotImplementedError

decrypt

1
2
3
4
5
6
7
decrypt(
    ciphertext: bytes | SupportsBytes,
    *,
    iv: bytes | SupportsBytes,
    auth_tag: bytes | SupportsBytes,
    aad: bytes | SupportsBytes | None = None
) -> BinaPy

Decrypt and verify a ciphertext with Authenticated Encryption.

This needs:

  • the raw encrypted Data (ciphertext) and Authentication Tag (auth_tag) that were produced by encryption,
  • the same Initialisation Vector (iv) and optional Additional Authentication Data that were provided for encryption.

Returns the resulting clear text data.

Parameters:

Name Type Description Default
ciphertext bytes | SupportsBytes

the data to decrypt

required
iv bytes | SupportsBytes

the Initialisation Vector to use. Must be the same one used during encryption

required
auth_tag bytes | SupportsBytes

the authentication tag

required
aad bytes | SupportsBytes | None

the Additional Authentication Data. Must be the same one used during encryption

None

Returns:

Type Description
BinaPy

the deciphered data

Source code in jwskate/jwa/base.py
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
def decrypt(
    self,
    ciphertext: bytes | SupportsBytes,
    *,
    iv: bytes | SupportsBytes,
    auth_tag: bytes | SupportsBytes,
    aad: bytes | SupportsBytes | None = None,
) -> BinaPy:
    """Decrypt and verify a ciphertext with Authenticated Encryption.

    This needs:

    - the raw encrypted Data (`ciphertext`) and Authentication Tag (`auth_tag`) that were produced by encryption,
    - the same Initialisation Vector (`iv`) and optional Additional Authentication Data that were provided for
    encryption.

    Returns the resulting clear text data.

    Args:
      ciphertext: the data to decrypt
      iv: the Initialisation Vector to use. Must be the same one used during encryption
      auth_tag: the authentication tag
      aad: the Additional Authentication Data. Must be the same one used during encryption

    Returns:
      the deciphered data

    """
    raise NotImplementedError

with_random_key classmethod

1
with_random_key() -> Self

Initialize this alg with a random key.

Returns:

Type Description
Self

a subclass of BaseAESEncryptionAlg initialized with a randomly generated key

Source code in jwskate/jwa/base.py
323
324
325
326
327
328
329
330
331
332
@classmethod
@override
def with_random_key(cls) -> Self:
    """Initialize this alg with a random key.

    Returns:
        a subclass of `BaseAESEncryptionAlg` initialized with a randomly generated key

    """
    return cls(cls.generate_key())

BaseAlg

Base class for all algorithms.

An algorithm has a name and a description, whose reference is found in IANA JOSE registry.

Source code in jwskate/jwa/base.py
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
class BaseAlg:
    """Base class for all algorithms.

    An algorithm has a `name` and a `description`, whose reference is found in [IANA JOSE registry][IANA].

    [IANA]: https://www.iana.org/assignments/jose/jose.xhtml#web-signature-encryption-algorithms

    """

    use: str
    """Alg use ('sig' or 'enc')"""

    name: str
    """Technical name of the algorithm."""
    description: str
    """Description of the algorithm (human readable)"""
    read_only: bool = False
    """For algs that are considered insecure, set to True to allow only signature verification or
    decryption of existing data, but don't allow new signatures or encryption."""

    def __repr__(self) -> str:
        """Use the name of the alg as repr."""
        return self.name

    @classmethod
    def with_random_key(cls) -> Self:
        """Initialize an instance of this alg with a randomly-generated key."""
        raise NotImplementedError()

use instance-attribute

1
use: str

Alg use ('sig' or 'enc')

name instance-attribute

1
name: str

Technical name of the algorithm.

description instance-attribute

1
description: str

Description of the algorithm (human readable)

read_only class-attribute instance-attribute

1
read_only: bool = False

For algs that are considered insecure, set to True to allow only signature verification or decryption of existing data, but don't allow new signatures or encryption.

__repr__

1
__repr__() -> str

Use the name of the alg as repr.

Source code in jwskate/jwa/base.py
46
47
48
def __repr__(self) -> str:
    """Use the name of the alg as repr."""
    return self.name

with_random_key classmethod

1
with_random_key() -> Self

Initialize an instance of this alg with a randomly-generated key.

Source code in jwskate/jwa/base.py
50
51
52
53
@classmethod
def with_random_key(cls) -> Self:
    """Initialize an instance of this alg with a randomly-generated key."""
    raise NotImplementedError()

BaseAsymmetricAlg

Bases: Generic[Kpriv, Kpub], BaseAlg

Base class for asymmetric algorithms. Those can be initialised with a private or public key.

The available cryptographic operations will depend on the alg and the provided key type.

Parameters:

Name Type Description Default
key Kpriv | Kpub

the key to use.

required
Source code in jwskate/jwa/base.py
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
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
173
174
175
176
177
178
179
180
181
182
183
184
185
186
class BaseAsymmetricAlg(Generic[Kpriv, Kpub], BaseAlg):
    """Base class for asymmetric algorithms. Those can be initialised with a private or public key.

    The available cryptographic operations will depend on the alg and
    the provided key type.

    Args:
        key: the key to use.

    """

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

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

    @classmethod
    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.

        Args:
          key: the key to use.

        Returns:
          Returns None. Raises an exception if the key is not suitable.

        Raises:
            Exception: if the key is not suitable for use with this alg class

        """

    @contextmanager
    def private_key_required(self) -> Iterator[Kpriv]:
        """Check if this alg is initialised with a private key, as a context manager.

        Yields:
            the private key

        Raises:
            PrivateKeyRequired: if the configured key is not private

        """
        if not isinstance(self.key, self.private_key_class):
            raise PrivateKeyRequired()
        yield self.key  # type: ignore[misc]

    @contextmanager
    def public_key_required(self) -> Iterator[Kpub]:
        """Check if this alg is initialised with a public key, as a context manager.

        Yields:
            The public key

        Raises:
            PublicKeyRequired: if the configured key is private

        """
        if not isinstance(self.key, self.public_key_class):
            raise PublicKeyRequired()
        yield self.key  # type: ignore[misc]

    def public_key(self) -> Kpub:
        """Return the public key matching the private key."""
        if hasattr(self.key, "public_key"):
            return self.key.public_key()  # type: ignore[no-any-return]
        raise NotImplementedError()

    def public_alg(self) -> Self:
        """Return an alg instance initialised with the public key."""
        with self.private_key_required():
            return self.__class__(self.public_key())

check_key classmethod

1
check_key(key: Kpriv | Kpub) -> None

Check that a given key is suitable for this alg class.

This must be implemented by subclasses as required.

Parameters:

Name Type Description Default
key Kpriv | Kpub

the key to use.

required

Returns:

Type Description
None

Returns None. Raises an exception if the key is not suitable.

Raises:

Type Description
Exception

if the key is not suitable for use with this alg class

Source code in jwskate/jwa/base.py
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
@classmethod
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.

    Args:
      key: the key to use.

    Returns:
      Returns None. Raises an exception if the key is not suitable.

    Raises:
        Exception: if the key is not suitable for use with this alg class

    """

private_key_required

1
private_key_required() -> Iterator[Kpriv]

Check if this alg is initialised with a private key, as a context manager.

Yields:

Type Description
Kpriv

the private key

Raises:

Type Description
PrivateKeyRequired

if the configured key is not private

Source code in jwskate/jwa/base.py
147
148
149
150
151
152
153
154
155
156
157
158
159
160
@contextmanager
def private_key_required(self) -> Iterator[Kpriv]:
    """Check if this alg is initialised with a private key, as a context manager.

    Yields:
        the private key

    Raises:
        PrivateKeyRequired: if the configured key is not private

    """
    if not isinstance(self.key, self.private_key_class):
        raise PrivateKeyRequired()
    yield self.key  # type: ignore[misc]

public_key_required

1
public_key_required() -> Iterator[Kpub]

Check if this alg is initialised with a public key, as a context manager.

Yields:

Type Description
Kpub

The public key

Raises:

Type Description
PublicKeyRequired

if the configured key is private

Source code in jwskate/jwa/base.py
162
163
164
165
166
167
168
169
170
171
172
173
174
175
@contextmanager
def public_key_required(self) -> Iterator[Kpub]:
    """Check if this alg is initialised with a public key, as a context manager.

    Yields:
        The public key

    Raises:
        PublicKeyRequired: if the configured key is private

    """
    if not isinstance(self.key, self.public_key_class):
        raise PublicKeyRequired()
    yield self.key  # type: ignore[misc]

public_key

1
public_key() -> Kpub

Return the public key matching the private key.

Source code in jwskate/jwa/base.py
177
178
179
180
181
def public_key(self) -> Kpub:
    """Return the public key matching the private key."""
    if hasattr(self.key, "public_key"):
        return self.key.public_key()  # type: ignore[no-any-return]
    raise NotImplementedError()

public_alg

1
public_alg() -> Self

Return an alg instance initialised with the public key.

Source code in jwskate/jwa/base.py
183
184
185
186
def public_alg(self) -> Self:
    """Return an alg instance initialised with the public key."""
    with self.private_key_required():
        return self.__class__(self.public_key())

BaseKeyManagementAlg

Bases: BaseAlg

Base class for Key Management algorithms.

Source code in jwskate/jwa/base.py
335
336
337
338
class BaseKeyManagementAlg(BaseAlg):
    """Base class for Key Management algorithms."""

    use = "enc"

BaseSignatureAlg

Bases: BaseAlg

Base class for signature algorithms.

Source code in jwskate/jwa/base.py
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
class BaseSignatureAlg(BaseAlg):
    """Base class for signature algorithms."""

    use = "sig"
    hashing_alg: hashes.HashAlgorithm

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

        Args:
          data: raw data to sign

        Returns:
          the raw signature

        """
        raise NotImplementedError

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

        Args:
          data: the raw data to verify
          signature: the raw signature

        Returns:
          `True` if the signature matches, `False` otherwise.

        """
        raise NotImplementedError

sign

1
sign(data: bytes | SupportsBytes) -> BinaPy

Sign arbitrary data, return the signature.

Parameters:

Name Type Description Default
data bytes | SupportsBytes

raw data to sign

required

Returns:

Type Description
BinaPy

the raw signature

Source code in jwskate/jwa/base.py
195
196
197
198
199
200
201
202
203
204
205
def sign(self, data: bytes | SupportsBytes) -> BinaPy:
    """Sign arbitrary data, return the signature.

    Args:
      data: raw data to sign

    Returns:
      the raw signature

    """
    raise NotImplementedError

verify

1
2
3
4
verify(
    data: bytes | SupportsBytes,
    signature: bytes | SupportsBytes,
) -> bool

Verify a signature against some data.

Parameters:

Name Type Description Default
data bytes | SupportsBytes

the raw data to verify

required
signature bytes | SupportsBytes

the raw signature

required

Returns:

Type Description
bool

True if the signature matches, False otherwise.

Source code in jwskate/jwa/base.py
207
208
209
210
211
212
213
214
215
216
217
218
def verify(self, data: bytes | SupportsBytes, signature: bytes | SupportsBytes) -> bool:
    """Verify a signature against some data.

    Args:
      data: the raw data to verify
      signature: the raw signature

    Returns:
      `True` if the signature matches, `False` otherwise.

    """
    raise NotImplementedError

BaseSymmetricAlg

Bases: BaseAlg

Base class for Symmetric algorithms (using a raw bytes key).

Parameters:

Name Type Description Default
key bytes

the key to use for cryptographic operations

required
Source code in jwskate/jwa/base.py
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
class BaseSymmetricAlg(BaseAlg):
    """Base class for Symmetric algorithms (using a raw bytes key).

    Args:
        key: the key to use for cryptographic operations

    """

    def __init__(self, key: bytes):
        self.check_key(key)
        self.key = key

    @classmethod
    def check_key(cls, key: bytes) -> None:
        """Check that a given key is suitable for this alg class.

        This raises an exception if the key is not suitable.
        This method must be implemented by subclasses as required.

        Args:
          key: the key to check for this alg class

        Returns:
          Returns `None`. Raises an exception if the key is not suitable

        Raises:
            InvalidKey: if the key is not suitable for this algorithm

        """
        pass

    @classmethod
    def supports_key(cls, key: bytes) -> bool:
        """Return `True` if the given key is suitable for this alg class, or `False` otherwise.

        This is a convenience wrapper around `check_key(key)`.

        Args:
          key: the key to check for this alg class

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

        """
        try:
            cls.check_key(key)
        except InvalidKey:
            return False
        else:
            return True

check_key classmethod

1
check_key(key: bytes) -> None

Check that a given key is suitable for this alg class.

This raises an exception if the key is not suitable. This method must be implemented by subclasses as required.

Parameters:

Name Type Description Default
key bytes

the key to check for this alg class

required

Returns:

Type Description
None

Returns None. Raises an exception if the key is not suitable

Raises:

Type Description
InvalidKey

if the key is not suitable for this algorithm

Source code in jwskate/jwa/base.py
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
@classmethod
def check_key(cls, key: bytes) -> None:
    """Check that a given key is suitable for this alg class.

    This raises an exception if the key is not suitable.
    This method must be implemented by subclasses as required.

    Args:
      key: the key to check for this alg class

    Returns:
      Returns `None`. Raises an exception if the key is not suitable

    Raises:
        InvalidKey: if the key is not suitable for this algorithm

    """
    pass

supports_key classmethod

1
supports_key(key: bytes) -> bool

Return True if the given key is suitable for this alg class, or False otherwise.

This is a convenience wrapper around check_key(key).

Parameters:

Name Type Description Default
key bytes

the key to check for this alg class

required

Returns:

Type Description
bool

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

Source code in jwskate/jwa/base.py
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
@classmethod
def supports_key(cls, key: bytes) -> bool:
    """Return `True` if the given key is suitable for this alg class, or `False` otherwise.

    This is a convenience wrapper around `check_key(key)`.

    Args:
      key: the key to check for this alg class

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

    """
    try:
        cls.check_key(key)
    except InvalidKey:
        return False
    else:
        return True

MismatchingAuthTag

Bases: InvalidTag

Raised during decryption, when the Authentication Tag doesn't match the expected value.

Source code in jwskate/jwa/base.py
341
342
class MismatchingAuthTag(cryptography.exceptions.InvalidTag):
    """Raised during decryption, when the Authentication Tag doesn't match the expected value."""

PrivateKeyRequired

Bases: AttributeError

Raised when a public key is provided for an operation that requires a private key.

Source code in jwskate/jwa/base.py
14
15
class PrivateKeyRequired(AttributeError):
    """Raised when a public key is provided for an operation that requires a private key."""

PublicKeyRequired

Bases: AttributeError

Raised when a private key is provided for an operation that requires a public key.

Source code in jwskate/jwa/base.py
18
19
class PublicKeyRequired(AttributeError):
    """Raised when a private key is provided for an operation that requires a public key."""

EllipticCurve dataclass

A descriptive class for Elliptic Curves.

Elliptic Curves have a name, a cryptography.ec.EllipticCurve, and a coordinate size.

Source code in jwskate/jwa/ec.py
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@dataclass
class EllipticCurve:
    """A descriptive class for Elliptic Curves.

    Elliptic Curves have a name, a `cryptography.ec.EllipticCurve`, and a coordinate size.

    """

    name: str
    """Curve name as defined in [IANA JOSE](https://www.iana.org/assignments/jose/jose.xhtml#web-
    key-elliptic-curve).

    This name will appear in `alg` or `enc` fields in JOSE headers.
    """

    cryptography_curve: ec.EllipticCurve
    """`cryptography` curve instance."""

    coordinate_size: int
    """Coordinate size, in bytes."""

    instances: ClassVar[dict[str, EllipticCurve]] = {}
    """Registry of subclasses, in a {name: instance} mapping."""

    def __post_init__(self) -> None:
        """Automatically register subclasses in the instance registry."""
        self.instances[self.name] = self

name instance-attribute

1
name: str

Curve name as defined in IANA JOSE.

This name will appear in alg or enc fields in JOSE headers.

cryptography_curve instance-attribute

1
cryptography_curve: EllipticCurve

cryptography curve instance.

coordinate_size instance-attribute

1
coordinate_size: int

Coordinate size, in bytes.

instances class-attribute

1
instances: dict[str, EllipticCurve] = {}

Registry of subclasses, in a {name: instance} mapping.

__post_init__

1
__post_init__() -> None

Automatically register subclasses in the instance registry.

Source code in jwskate/jwa/ec.py
35
36
37
def __post_init__(self) -> None:
    """Automatically register subclasses in the instance registry."""
    self.instances[self.name] = self

A128CBC_HS256

Bases: BaseAesCbcHmacSha2

AES_128_CBC_HMAC_SHA_256.

Source code in jwskate/jwa/encryption/aescbchmac.py
166
167
168
169
170
171
172
173
174
175
class A128CBC_HS256(BaseAesCbcHmacSha2):  # noqa: N801
    """AES_128_CBC_HMAC_SHA_256."""

    name = "A128CBC-HS256"
    description = __doc__
    mac_key_size = 128
    aes_key_size = 128
    key_size = mac_key_size + aes_key_size
    tag_size = 16
    hash_alg = hashes.SHA256()

A128GCM

Bases: BaseAESGCM

AES GCM using 128-bit key.

Source code in jwskate/jwa/encryption/aesgcm.py
100
101
102
103
104
105
class A128GCM(BaseAESGCM):
    """AES GCM using 128-bit key."""

    name = "A128GCM"
    description = __doc__
    key_size = 128

A192CBC_HS384

Bases: BaseAesCbcHmacSha2

AES_192_CBC_HMAC_SHA_384.

Source code in jwskate/jwa/encryption/aescbchmac.py
178
179
180
181
182
183
184
185
186
187
class A192CBC_HS384(BaseAesCbcHmacSha2):  # noqa: N801
    """AES_192_CBC_HMAC_SHA_384."""

    name = "A192CBC-HS384"
    description = __doc__
    mac_key_size = 192
    aes_key_size = 192
    key_size = mac_key_size + aes_key_size
    tag_size = 24
    hash_alg = hashes.SHA384()

A192GCM

Bases: BaseAESGCM

AES GCM using 192-bit key.

Source code in jwskate/jwa/encryption/aesgcm.py
108
109
110
111
112
113
class A192GCM(BaseAESGCM):
    """AES GCM using 192-bit key."""

    name = "A192GCM"
    description = __doc__
    key_size = 192

A256CBC_HS512

Bases: BaseAesCbcHmacSha2

AES_256_CBC_HMAC_SHA_512.

Source code in jwskate/jwa/encryption/aescbchmac.py
190
191
192
193
194
195
196
197
198
199
class A256CBC_HS512(BaseAesCbcHmacSha2):  # noqa: N801
    """AES_256_CBC_HMAC_SHA_512."""

    name = "A256CBC-HS512"
    description = __doc__
    mac_key_size = 256
    aes_key_size = 256
    key_size = mac_key_size + aes_key_size
    tag_size = 32
    hash_alg = hashes.SHA512()

A256GCM

Bases: BaseAESGCM

AES GCM using 256-bit key.

Source code in jwskate/jwa/encryption/aesgcm.py
116
117
118
119
120
121
class A256GCM(BaseAESGCM):
    """AES GCM using 256-bit key."""

    name = "A256GCM"
    description = __doc__
    key_size = 256

A128GCMKW

Bases: BaseAesGcmKeyWrap

Key wrapping with AES GCM using 128-bit key.

Source code in jwskate/jwa/key_mgmt/aesgcmkw.py
70
71
72
73
74
75
class A128GCMKW(BaseAesGcmKeyWrap):
    """Key wrapping with AES GCM using 128-bit key."""

    name = "A128GCMKW"
    description = __doc__
    key_size = 128

A128KW

Bases: BaseAesKeyWrap

AES Key Wrap with default initial value using 128-bit key.

Source code in jwskate/jwa/key_mgmt/aeskw.py
71
72
73
74
75
76
class A128KW(BaseAesKeyWrap):
    """AES Key Wrap with default initial value using 128-bit key."""

    name = "A128KW"
    description = __doc__
    key_size = 128

A192GCMKW

Bases: BaseAesGcmKeyWrap

Key wrapping with AES GCM using 192-bit key.

Source code in jwskate/jwa/key_mgmt/aesgcmkw.py
78
79
80
81
82
83
class A192GCMKW(BaseAesGcmKeyWrap):
    """Key wrapping with AES GCM using 192-bit key."""

    name = "A192GCMKW"
    description = __doc__
    key_size = 192

A192KW

Bases: BaseAesKeyWrap

AES Key Wrap with default initial value using 192-bit key.

Source code in jwskate/jwa/key_mgmt/aeskw.py
79
80
81
82
83
84
class A192KW(BaseAesKeyWrap):
    """AES Key Wrap with default initial value using 192-bit key."""

    name = "A192KW"
    description = __doc__
    key_size = 192

A256GCMKW

Bases: BaseAesGcmKeyWrap

Key wrapping with AES GCM using 256-bit key.

Source code in jwskate/jwa/key_mgmt/aesgcmkw.py
86
87
88
89
90
91
class A256GCMKW(BaseAesGcmKeyWrap):
    """Key wrapping with AES GCM using 256-bit key."""

    name = "A256GCMKW"
    description = __doc__
    key_size = 256

A256KW

Bases: BaseAesKeyWrap

AES Key Wrap with default initial value using 256-bit key.

Source code in jwskate/jwa/key_mgmt/aeskw.py
87
88
89
90
91
92
class A256KW(BaseAesKeyWrap):
    """AES Key Wrap with default initial value using 256-bit key."""

    name = "A256KW"
    description = __doc__
    key_size = 256

BaseAesGcmKeyWrap

Bases: BaseAESGCM, BaseKeyManagementAlg

Base class for AES-GCM Key wrapping algorithms.

Source code in jwskate/jwa/key_mgmt/aesgcmkw.py
13
14
15
16
17
18
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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
class BaseAesGcmKeyWrap(BaseAESGCM, BaseKeyManagementAlg):
    """Base class for AES-GCM Key wrapping algorithms."""

    use = "enc"

    key_size: int
    """Required key size, in bits."""
    tag_size: int = 16
    """Authentication tag size, in bits."""
    iv_size: int = 96
    """Initialisation Vector size, in bits."""

    def wrap_key(self, plainkey: bytes | SupportsBytes, *, iv: bytes | SupportsBytes) -> tuple[BinaPy, BinaPy]:
        """Wrap a symmetric key, which is typically used as Content Encryption Key (CEK).

        This method is used by the sender of the encrypted message.

        This needs a random Initialisation Vector (`iv`) of the appropriate size,
        which you can generate using the classmethod `generate_iv()`.

        Args:
          plainkey: the key to wrap
          iv: the Initialisation Vector to use

        Returns:
          a tuple (wrapped_key, authentication_tag)

        """
        return self.encrypt(plainkey, iv=iv)

    def unwrap_key(
        self,
        cipherkey: bytes | SupportsBytes,
        *,
        tag: bytes | SupportsBytes,
        iv: bytes | SupportsBytes,
    ) -> BinaPy:
        """Unwrap a symmetric key.

        This method is used by the recipient of an encrypted message.

        This requires:
        - the same IV that was provided during encryption
        - the same Authentication Tag that was generated during encryption

        Args:
          cipherkey: the wrapped key
          tag: the authentication tag
          iv: the Initialisation Vector

        Returns:
          the unwrapped key.

        """
        return self.decrypt(cipherkey, auth_tag=tag, iv=iv)

key_size instance-attribute

1
key_size: int

Required key size, in bits.

tag_size class-attribute instance-attribute

1
tag_size: int = 16

Authentication tag size, in bits.

iv_size class-attribute instance-attribute

1
iv_size: int = 96

Initialisation Vector size, in bits.

wrap_key

1
2
3
4
5
wrap_key(
    plainkey: bytes | SupportsBytes,
    *,
    iv: bytes | SupportsBytes
) -> tuple[BinaPy, BinaPy]

Wrap a symmetric key, which is typically used as Content Encryption Key (CEK).

This method is used by the sender of the encrypted message.

This needs a random Initialisation Vector (iv) of the appropriate size, which you can generate using the classmethod generate_iv().

Parameters:

Name Type Description Default
plainkey bytes | SupportsBytes

the key to wrap

required
iv bytes | SupportsBytes

the Initialisation Vector to use

required

Returns:

Type Description
tuple[BinaPy, BinaPy]

a tuple (wrapped_key, authentication_tag)

Source code in jwskate/jwa/key_mgmt/aesgcmkw.py
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
def wrap_key(self, plainkey: bytes | SupportsBytes, *, iv: bytes | SupportsBytes) -> tuple[BinaPy, BinaPy]:
    """Wrap a symmetric key, which is typically used as Content Encryption Key (CEK).

    This method is used by the sender of the encrypted message.

    This needs a random Initialisation Vector (`iv`) of the appropriate size,
    which you can generate using the classmethod `generate_iv()`.

    Args:
      plainkey: the key to wrap
      iv: the Initialisation Vector to use

    Returns:
      a tuple (wrapped_key, authentication_tag)

    """
    return self.encrypt(plainkey, iv=iv)

unwrap_key

1
2
3
4
5
6
unwrap_key(
    cipherkey: bytes | SupportsBytes,
    *,
    tag: bytes | SupportsBytes,
    iv: bytes | SupportsBytes
) -> BinaPy

Unwrap a symmetric key.

This method is used by the recipient of an encrypted message.

This requires: - the same IV that was provided during encryption - the same Authentication Tag that was generated during encryption

Parameters:

Name Type Description Default
cipherkey bytes | SupportsBytes

the wrapped key

required
tag bytes | SupportsBytes

the authentication tag

required
iv bytes | SupportsBytes

the Initialisation Vector

required

Returns:

Type Description
BinaPy

the unwrapped key.

Source code in jwskate/jwa/key_mgmt/aesgcmkw.py
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
def unwrap_key(
    self,
    cipherkey: bytes | SupportsBytes,
    *,
    tag: bytes | SupportsBytes,
    iv: bytes | SupportsBytes,
) -> BinaPy:
    """Unwrap a symmetric key.

    This method is used by the recipient of an encrypted message.

    This requires:
    - the same IV that was provided during encryption
    - the same Authentication Tag that was generated during encryption

    Args:
      cipherkey: the wrapped key
      tag: the authentication tag
      iv: the Initialisation Vector

    Returns:
      the unwrapped key.

    """
    return self.decrypt(cipherkey, auth_tag=tag, iv=iv)

BaseAesKeyWrap

Bases: BaseKeyManagementAlg, BaseSymmetricAlg

Base class for AES KW algorithms.

Source code in jwskate/jwa/key_mgmt/aeskw.py
14
15
16
17
18
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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
class BaseAesKeyWrap(BaseKeyManagementAlg, BaseSymmetricAlg):
    """Base class for AES KW algorithms."""

    key_size: int
    """Required AES key size in bits."""

    @classmethod
    @override
    def check_key(cls, key: bytes) -> None:
        """Check that a key is valid for usage with this algorithm.

        To be valid, a key must be `bytes` and be of appropriate length (128, 192 or 256 bits).

        Args:
          key: a key to check

        Raises:
            ValueError: if the key is not appropriate

        """
        if not isinstance(key, bytes) or len(key) * 8 != cls.key_size:
            msg = f"Key must be {cls.key_size} bits."
            raise InvalidKey(msg)

    @classmethod
    @override
    def with_random_key(cls) -> Self:
        return cls(BinaPy.random_bits(cls.key_size))

    def wrap_key(self, plainkey: bytes) -> BinaPy:
        """Wrap a key.

        Args:
          plainkey: the key to wrap.

        Returns:
          BinaPy: the wrapped key.

        """
        return BinaPy(keywrap.aes_key_wrap(self.key, plainkey))

    def unwrap_key(self, cipherkey: bytes | SupportsBytes) -> BinaPy:
        """Unwrap a key.

        Args:
          cipherkey: the wrapped key.

        Returns:
          BinaPy: the unwrapped key.

        """
        if not isinstance(cipherkey, bytes):
            cipherkey = bytes(cipherkey)

        return BinaPy(keywrap.aes_key_unwrap(self.key, cipherkey))

key_size instance-attribute

1
key_size: int

Required AES key size in bits.

check_key classmethod

1
check_key(key: bytes) -> None

Check that a key is valid for usage with this algorithm.

To be valid, a key must be bytes and be of appropriate length (128, 192 or 256 bits).

Parameters:

Name Type Description Default
key bytes

a key to check

required

Raises:

Type Description
ValueError

if the key is not appropriate

Source code in jwskate/jwa/key_mgmt/aeskw.py
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@classmethod
@override
def check_key(cls, key: bytes) -> None:
    """Check that a key is valid for usage with this algorithm.

    To be valid, a key must be `bytes` and be of appropriate length (128, 192 or 256 bits).

    Args:
      key: a key to check

    Raises:
        ValueError: if the key is not appropriate

    """
    if not isinstance(key, bytes) or len(key) * 8 != cls.key_size:
        msg = f"Key must be {cls.key_size} bits."
        raise InvalidKey(msg)

wrap_key

1
wrap_key(plainkey: bytes) -> BinaPy

Wrap a key.

Parameters:

Name Type Description Default
plainkey bytes

the key to wrap.

required

Returns:

Name Type Description
BinaPy BinaPy

the wrapped key.

Source code in jwskate/jwa/key_mgmt/aeskw.py
43
44
45
46
47
48
49
50
51
52
53
def wrap_key(self, plainkey: bytes) -> BinaPy:
    """Wrap a key.

    Args:
      plainkey: the key to wrap.

    Returns:
      BinaPy: the wrapped key.

    """
    return BinaPy(keywrap.aes_key_wrap(self.key, plainkey))

unwrap_key

1
unwrap_key(cipherkey: bytes | SupportsBytes) -> BinaPy

Unwrap a key.

Parameters:

Name Type Description Default
cipherkey bytes | SupportsBytes

the wrapped key.

required

Returns:

Name Type Description
BinaPy BinaPy

the unwrapped key.

Source code in jwskate/jwa/key_mgmt/aeskw.py
55
56
57
58
59
60
61
62
63
64
65
66
67
68
def unwrap_key(self, cipherkey: bytes | SupportsBytes) -> BinaPy:
    """Unwrap a key.

    Args:
      cipherkey: the wrapped key.

    Returns:
      BinaPy: the unwrapped key.

    """
    if not isinstance(cipherkey, bytes):
        cipherkey = bytes(cipherkey)

    return BinaPy(keywrap.aes_key_unwrap(self.key, cipherkey))

BaseEcdhEs_AesKw

Bases: EcdhEs

Base class for ECDH-ES+AESKW algorithms.

Source code in jwskate/jwa/key_mgmt/ecdh.py
210
211
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
240
241
242
243
244
245
246
247
248
249
250
251
252
253
class BaseEcdhEs_AesKw(EcdhEs):  # noqa: N801
    """Base class for ECDH-ES+AESKW algorithms."""

    kwalg: type[BaseAesKeyWrap]

    def wrap_key_with_epk(
        self,
        plainkey: bytes,
        ephemeral_private_key: ec.EllipticCurvePrivateKey | x25519.X25519PrivateKey | x448.X448PrivateKey,
        **headers: Any,
    ) -> BinaPy:
        """Wrap a key for content encryption.

        Args:
          plainkey: the key to wrap
          ephemeral_private_key: the EPK to use
          **headers: additional headers for CEK derivation

        Returns:
            the wrapped CEK

        """
        aes_key = self.sender_key(ephemeral_private_key, key_size=self.kwalg.key_size, **headers)
        return self.kwalg(aes_key).wrap_key(plainkey)

    def unwrap_key_with_epk(
        self,
        cipherkey: bytes | SupportsBytes,
        ephemeral_public_key: ec.EllipticCurvePublicKey | x25519.X25519PublicKey | x448.X448PublicKey,
        **headers: Any,
    ) -> BinaPy:
        """Unwrap a key for content decryption.

        Args:
          cipherkey: the wrapped key
          ephemeral_public_key: the EPK
          **headers: additional headers for CEK derivation

        Returns:
            the unwrapped key

        """
        aes_key = self.recipient_key(ephemeral_public_key, key_size=self.kwalg.key_size, **headers)
        return self.kwalg(aes_key).unwrap_key(cipherkey)

wrap_key_with_epk

1
2
3
4
5
6
7
8
9
wrap_key_with_epk(
    plainkey: bytes,
    ephemeral_private_key: (
        ec.EllipticCurvePrivateKey
        | x25519.X25519PrivateKey
        | x448.X448PrivateKey
    ),
    **headers: Any
) -> BinaPy

Wrap a key for content encryption.

Parameters:

Name Type Description Default
plainkey bytes

the key to wrap

required
ephemeral_private_key EllipticCurvePrivateKey | X25519PrivateKey | X448PrivateKey

the EPK to use

required
**headers Any

additional headers for CEK derivation

{}

Returns:

Type Description
BinaPy

the wrapped CEK

Source code in jwskate/jwa/key_mgmt/ecdh.py
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
def wrap_key_with_epk(
    self,
    plainkey: bytes,
    ephemeral_private_key: ec.EllipticCurvePrivateKey | x25519.X25519PrivateKey | x448.X448PrivateKey,
    **headers: Any,
) -> BinaPy:
    """Wrap a key for content encryption.

    Args:
      plainkey: the key to wrap
      ephemeral_private_key: the EPK to use
      **headers: additional headers for CEK derivation

    Returns:
        the wrapped CEK

    """
    aes_key = self.sender_key(ephemeral_private_key, key_size=self.kwalg.key_size, **headers)
    return self.kwalg(aes_key).wrap_key(plainkey)

unwrap_key_with_epk

1
2
3
4
5
6
7
8
9
unwrap_key_with_epk(
    cipherkey: bytes | SupportsBytes,
    ephemeral_public_key: (
        ec.EllipticCurvePublicKey
        | x25519.X25519PublicKey
        | x448.X448PublicKey
    ),
    **headers: Any
) -> BinaPy

Unwrap a key for content decryption.

Parameters:

Name Type Description Default
cipherkey bytes | SupportsBytes

the wrapped key

required
ephemeral_public_key EllipticCurvePublicKey | X25519PublicKey | X448PublicKey

the EPK

required
**headers Any

additional headers for CEK derivation

{}

Returns:

Type Description
BinaPy

the unwrapped key

Source code in jwskate/jwa/key_mgmt/ecdh.py
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
def unwrap_key_with_epk(
    self,
    cipherkey: bytes | SupportsBytes,
    ephemeral_public_key: ec.EllipticCurvePublicKey | x25519.X25519PublicKey | x448.X448PublicKey,
    **headers: Any,
) -> BinaPy:
    """Unwrap a key for content decryption.

    Args:
      cipherkey: the wrapped key
      ephemeral_public_key: the EPK
      **headers: additional headers for CEK derivation

    Returns:
        the unwrapped key

    """
    aes_key = self.recipient_key(ephemeral_public_key, key_size=self.kwalg.key_size, **headers)
    return self.kwalg(aes_key).unwrap_key(cipherkey)

BasePbes2

Bases: BaseKeyManagementAlg

Base class for PBES2 based algorithms.

PBES2 derives a cryptographic key from a human-provided password.

Parameters:

Name Type Description Default
password SupportsBytes | bytes | str

the encryption/decryption password to use

required
Source code in jwskate/jwa/key_mgmt/pbes2.py
 16
 17
 18
 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
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
class BasePbes2(BaseKeyManagementAlg):
    """Base class for PBES2 based algorithms.

    PBES2 derives a cryptographic key from a human-provided password.

    Args:
        password: the encryption/decryption password to use

    """

    kwalg: type[BaseAesKeyWrap]
    hash_alg: hashes.HashAlgorithm

    MIN_SALT_SIZE = 8

    def __init__(self, password: SupportsBytes | bytes | str):
        if isinstance(password, str):
            password = password.encode("utf-8")
        if not isinstance(password, bytes):
            password = bytes(password)
        self.password = password

    @classmethod
    def generate_salt(cls, size: int = 12) -> BinaPy:
        """Generate a salt that is suitable for use for encryption.

        Args:
          size: size of the generated salt, in bytes

        Returns:
            the generated salt

        Raises:
            ValueError: if the salt is less than 8 bytes long

        """
        if size < cls.MIN_SALT_SIZE:
            msg = f"salts used for PBES2 must be at least {cls.MIN_SALT_SIZE} bytes long"
            raise ValueError(msg)
        return BinaPy.random(size)

    def derive(self, *, salt: bytes, count: int) -> BinaPy:
        """Derive an encryption key.

        Derivation is based on the configured password, a given salt and the number of
        PBKDF iterations.

        Args:
          salt: the generated salt
          count: number of PBKDF iterations

        Returns:
            the generated encryption/decryption key

        """
        full_salt = self.name.encode() + b"\0" + salt
        pbkdf = pbkdf2.PBKDF2HMAC(
            algorithm=self.hash_alg,
            length=self.kwalg.key_size // 8,
            salt=full_salt,
            iterations=count,
        )
        return BinaPy(pbkdf.derive(self.password))

    def wrap_key(self, plainkey: bytes, *, salt: bytes, count: int) -> BinaPy:
        """Wrap a key using this alg.

        Args:
          plainkey: the key to wrap
          salt: the salt to use
          count: the number of PBKDF iterations

        Returns:
            the wrapped key

        """
        aes_key = self.derive(salt=salt, count=count)
        return BinaPy(self.kwalg(aes_key).wrap_key(plainkey))

    def unwrap_key(self, cipherkey: bytes, *, salt: bytes, count: int) -> BinaPy:
        """Unwrap a key using this alg.

        Args:
          cipherkey: the wrapped key
          salt: the salt to use
          count: the number of PBKDF iterations

        Returns:
            the unwrapped key

        """
        aes_key = self.derive(salt=salt, count=count)
        return BinaPy(self.kwalg(aes_key).unwrap_key(cipherkey))

generate_salt classmethod

1
generate_salt(size: int = 12) -> BinaPy

Generate a salt that is suitable for use for encryption.

Parameters:

Name Type Description Default
size int

size of the generated salt, in bytes

12

Returns:

Type Description
BinaPy

the generated salt

Raises:

Type Description
ValueError

if the salt is less than 8 bytes long

Source code in jwskate/jwa/key_mgmt/pbes2.py
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
@classmethod
def generate_salt(cls, size: int = 12) -> BinaPy:
    """Generate a salt that is suitable for use for encryption.

    Args:
      size: size of the generated salt, in bytes

    Returns:
        the generated salt

    Raises:
        ValueError: if the salt is less than 8 bytes long

    """
    if size < cls.MIN_SALT_SIZE:
        msg = f"salts used for PBES2 must be at least {cls.MIN_SALT_SIZE} bytes long"
        raise ValueError(msg)
    return BinaPy.random(size)

derive

1
derive(*, salt: bytes, count: int) -> BinaPy

Derive an encryption key.

Derivation is based on the configured password, a given salt and the number of PBKDF iterations.

Parameters:

Name Type Description Default
salt bytes

the generated salt

required
count int

number of PBKDF iterations

required

Returns:

Type Description
BinaPy

the generated encryption/decryption key

Source code in jwskate/jwa/key_mgmt/pbes2.py
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
def derive(self, *, salt: bytes, count: int) -> BinaPy:
    """Derive an encryption key.

    Derivation is based on the configured password, a given salt and the number of
    PBKDF iterations.

    Args:
      salt: the generated salt
      count: number of PBKDF iterations

    Returns:
        the generated encryption/decryption key

    """
    full_salt = self.name.encode() + b"\0" + salt
    pbkdf = pbkdf2.PBKDF2HMAC(
        algorithm=self.hash_alg,
        length=self.kwalg.key_size // 8,
        salt=full_salt,
        iterations=count,
    )
    return BinaPy(pbkdf.derive(self.password))

wrap_key

1
2
3
wrap_key(
    plainkey: bytes, *, salt: bytes, count: int
) -> BinaPy

Wrap a key using this alg.

Parameters:

Name Type Description Default
plainkey bytes

the key to wrap

required
salt bytes

the salt to use

required
count int

the number of PBKDF iterations

required

Returns:

Type Description
BinaPy

the wrapped key

Source code in jwskate/jwa/key_mgmt/pbes2.py
80
81
82
83
84
85
86
87
88
89
90
91
92
93
def wrap_key(self, plainkey: bytes, *, salt: bytes, count: int) -> BinaPy:
    """Wrap a key using this alg.

    Args:
      plainkey: the key to wrap
      salt: the salt to use
      count: the number of PBKDF iterations

    Returns:
        the wrapped key

    """
    aes_key = self.derive(salt=salt, count=count)
    return BinaPy(self.kwalg(aes_key).wrap_key(plainkey))

unwrap_key

1
2
3
unwrap_key(
    cipherkey: bytes, *, salt: bytes, count: int
) -> BinaPy

Unwrap a key using this alg.

Parameters:

Name Type Description Default
cipherkey bytes

the wrapped key

required
salt bytes

the salt to use

required
count int

the number of PBKDF iterations

required

Returns:

Type Description
BinaPy

the unwrapped key

Source code in jwskate/jwa/key_mgmt/pbes2.py
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
def unwrap_key(self, cipherkey: bytes, *, salt: bytes, count: int) -> BinaPy:
    """Unwrap a key using this alg.

    Args:
      cipherkey: the wrapped key
      salt: the salt to use
      count: the number of PBKDF iterations

    Returns:
        the unwrapped key

    """
    aes_key = self.derive(salt=salt, count=count)
    return BinaPy(self.kwalg(aes_key).unwrap_key(cipherkey))

BaseRsaKeyWrap

Bases: BaseKeyManagementAlg, BaseAsymmetricAlg[RSAPrivateKey, RSAPublicKey]

Base class for RSA Key Wrapping algorithms.

Source code in jwskate/jwa/key_mgmt/rsa.py
15
16
17
18
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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
class BaseRsaKeyWrap(
    BaseKeyManagementAlg,
    BaseAsymmetricAlg[rsa.RSAPrivateKey, rsa.RSAPublicKey],
):
    """Base class for RSA Key Wrapping algorithms."""

    padding: Any

    name: str
    description: str

    private_key_class = rsa.RSAPrivateKey
    public_key_class = rsa.RSAPublicKey

    min_key_size: int = 2048

    @classmethod
    @override
    def with_random_key(cls) -> Self:
        return cls(rsa.generate_private_key(public_exponent=65537, key_size=cls.min_key_size))

    def wrap_key(self, plainkey: bytes) -> BinaPy:
        """Wrap a symmetric key using this algorithm.

        Args:
          plainkey: the symmetric key to wrap

        Returns:
            the wrapped key

        Raises:
            PublicKeyRequired: if this algorithm is initialized with a private key instead of a public key

        """
        if self.read_only:
            msg = "Due to security reasons, this algorithm is only usable for decryption."
            raise NotImplementedError(msg)
        with self.public_key_required() as key:
            return BinaPy(key.encrypt(plainkey, self.padding))

    def unwrap_key(self, cipherkey: bytes | SupportsBytes) -> BinaPy:
        """Unwrap a symmetric key with this alg.

        Args:
          cipherkey: the wrapped key

        Returns:
            the unwrapped clear-text key
        Raises:
            PrivateKeyRequired: if this alg is initialized with a public key instead of a private key

        """
        if not isinstance(cipherkey, bytes):
            cipherkey = bytes(cipherkey)

        with self.private_key_required() as key:
            return BinaPy(key.decrypt(cipherkey, self.padding))

wrap_key

1
wrap_key(plainkey: bytes) -> BinaPy

Wrap a symmetric key using this algorithm.

Parameters:

Name Type Description Default
plainkey bytes

the symmetric key to wrap

required

Returns:

Type Description
BinaPy

the wrapped key

Raises:

Type Description
PublicKeyRequired

if this algorithm is initialized with a private key instead of a public key

Source code in jwskate/jwa/key_mgmt/rsa.py
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
def wrap_key(self, plainkey: bytes) -> BinaPy:
    """Wrap a symmetric key using this algorithm.

    Args:
      plainkey: the symmetric key to wrap

    Returns:
        the wrapped key

    Raises:
        PublicKeyRequired: if this algorithm is initialized with a private key instead of a public key

    """
    if self.read_only:
        msg = "Due to security reasons, this algorithm is only usable for decryption."
        raise NotImplementedError(msg)
    with self.public_key_required() as key:
        return BinaPy(key.encrypt(plainkey, self.padding))

unwrap_key

1
unwrap_key(cipherkey: bytes | SupportsBytes) -> BinaPy

Unwrap a symmetric key with this alg.

Parameters:

Name Type Description Default
cipherkey bytes | SupportsBytes

the wrapped key

required

Returns:

Type Description
BinaPy

the unwrapped clear-text key

Source code in jwskate/jwa/key_mgmt/rsa.py
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
def unwrap_key(self, cipherkey: bytes | SupportsBytes) -> BinaPy:
    """Unwrap a symmetric key with this alg.

    Args:
      cipherkey: the wrapped key

    Returns:
        the unwrapped clear-text key
    Raises:
        PrivateKeyRequired: if this alg is initialized with a public key instead of a private key

    """
    if not isinstance(cipherkey, bytes):
        cipherkey = bytes(cipherkey)

    with self.private_key_required() as key:
        return BinaPy(key.decrypt(cipherkey, self.padding))

DirectKeyUse

Bases: BaseKeyManagementAlg, BaseSymmetricAlg

Direct use of a shared symmetric key as the CEK.

Source code in jwskate/jwa/key_mgmt/dir.py
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class DirectKeyUse(BaseKeyManagementAlg, BaseSymmetricAlg):
    """Direct use of a shared symmetric key as the CEK."""

    name = "dir"
    description = __doc__

    def direct_key(self, aesalg: type[BaseSymmetricAlg]) -> BinaPy:
        """Check that the current key is appropriate for a given alg and return that same key.

        Args:
          aesalg: the AES encryption alg to use

        Returns:
          the current configured key, as-is

        """
        aesalg.check_key(self.key)
        return BinaPy(self.key)

direct_key

1
direct_key(aesalg: type[BaseSymmetricAlg]) -> BinaPy

Check that the current key is appropriate for a given alg and return that same key.

Parameters:

Name Type Description Default
aesalg type[BaseSymmetricAlg]

the AES encryption alg to use

required

Returns:

Type Description
BinaPy

the current configured key, as-is

Source code in jwskate/jwa/key_mgmt/dir.py
16
17
18
19
20
21
22
23
24
25
26
27
def direct_key(self, aesalg: type[BaseSymmetricAlg]) -> BinaPy:
    """Check that the current key is appropriate for a given alg and return that same key.

    Args:
      aesalg: the AES encryption alg to use

    Returns:
      the current configured key, as-is

    """
    aesalg.check_key(self.key)
    return BinaPy(self.key)

EcdhEs

Bases: BaseKeyManagementAlg, BaseAsymmetricAlg[Union[EllipticCurvePrivateKey, X25519PrivateKey, X448PrivateKey], Union[EllipticCurvePublicKey, X25519PublicKey, X448PublicKey]]

Elliptic Curve Diffie-Hellman Ephemeral Static key agreement using Concat KDF.

Source code in jwskate/jwa/key_mgmt/ecdh.py
 18
 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
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 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
132
133
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
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
class EcdhEs(
    BaseKeyManagementAlg,
    BaseAsymmetricAlg[
        Union[ec.EllipticCurvePrivateKey, x25519.X25519PrivateKey, x448.X448PrivateKey],
        Union[ec.EllipticCurvePublicKey, x25519.X25519PublicKey, x448.X448PublicKey],
    ],
):
    """Elliptic Curve Diffie-Hellman Ephemeral Static key agreement using Concat KDF."""

    name = "ECDH-ES"
    description = __doc__
    public_key_class = (
        ec.EllipticCurvePublicKey,
        x25519.X25519PublicKey,
        x448.X448PublicKey,
    )
    private_key_class = (
        ec.EllipticCurvePrivateKey,
        x25519.X25519PrivateKey,
        x448.X448PrivateKey,
    )

    @classmethod
    @override
    def with_random_key(cls) -> Self:
        return cls(x25519.X25519PrivateKey.generate())

    @classmethod
    def otherinfo(cls, alg: str, apu: bytes, apv: bytes, key_size: int) -> BinaPy:
        """Build the "otherinfo" parameter for Concat KDF Hash.

        Args:
          alg: identifier for the encryption alg
          apu: Agreement PartyUInfo
          apv: Agreement PartyVInfo
          key_size: length of the generated key

        Returns:
            the "otherinfo" value

        """
        algorithm_id = BinaPy.from_int(len(alg), length=4) + BinaPy(alg)
        partyuinfo = BinaPy.from_int(len(apu), length=4) + apu
        partyvinfo = BinaPy.from_int(len(apv), length=4) + apv
        supppubinfo = BinaPy.from_int(key_size or key_size, length=4)
        otherinfo = b"".join((algorithm_id, partyuinfo, partyvinfo, supppubinfo))
        return BinaPy(otherinfo)

    @classmethod
    def ecdh(
        cls,
        private_key: ec.EllipticCurvePrivateKey | x25519.X25519PrivateKey | x448.X448PrivateKey,
        public_key: ec.EllipticCurvePublicKey | x25519.X25519PublicKey | x448.X448PublicKey,
    ) -> BinaPy:
        """Perform an Elliptic Curve Diffie-Hellman key exchange.

        This derives a shared key between a sender and a receiver, based on a public and a private key from each side.
        ECDH exchange produces the same key with either a sender private key and a recipient public key,
        or the matching sender public key and recipient private key.

        Args:
          private_key: a private EC key
          public_key: a public EC key

        Returns:
          a shared key

        """
        if isinstance(private_key, ec.EllipticCurvePrivateKey) and isinstance(public_key, ec.EllipticCurvePublicKey):
            shared_key = private_key.exchange(ec.ECDH(), public_key)
        elif isinstance(private_key, x25519.X25519PrivateKey) and isinstance(  # noqa: SIM114
            public_key,
            x25519.X25519PublicKey,
        ):
            shared_key = private_key.exchange(public_key)
        elif isinstance(private_key, x448.X448PrivateKey) and isinstance(public_key, x448.X448PublicKey):
            shared_key = private_key.exchange(public_key)
        else:
            msg = "Invalid or unsupported private/public key combination for ECDH"
            raise TypeError(
                msg,
                type(private_key),
                type(public_key),
            )
        return BinaPy(shared_key)

    @classmethod
    def derive(
        cls,
        *,
        private_key: ec.EllipticCurvePrivateKey | x25519.X25519PrivateKey | x448.X448PrivateKey,
        public_key: ec.EllipticCurvePublicKey | x25519.X25519PublicKey | x448.X448PublicKey,
        otherinfo: bytes,
        key_size: int,
    ) -> BinaPy:
        """Derive a key using ECDH and Concat KDF Hash.

        Args:
          private_key: the private key
          public_key: the public key
          otherinfo: the Concat KDF "otherinfo" parameter
          key_size: the expected CEK key size

        Returns:
            the derived key

        """
        shared_key = cls.ecdh(private_key, public_key)
        ckdf = ConcatKDFHash(algorithm=hashes.SHA256(), length=key_size // 8, otherinfo=otherinfo)
        return BinaPy(ckdf.derive(shared_key))

    def generate_ephemeral_key(
        self,
    ) -> ec.EllipticCurvePrivateKey | x25519.X25519PrivateKey | x448.X448PrivateKey:
        """Generate an ephemeral key that is suitable for use with this algorithm.

        Returns:
            a generated EllipticCurvePrivateKey, on the same curve as this algorithm key

        """
        if isinstance(self.key, (ec.EllipticCurvePrivateKey, ec.EllipticCurvePublicKey)):
            return ec.generate_private_key(self.key.curve)
        elif isinstance(self.key, (x25519.X25519PrivateKey, x25519.X25519PublicKey)):
            return x25519.X25519PrivateKey.generate()
        elif isinstance(self.key, (x448.X448PublicKey, x448.X448PrivateKey)):
            return x448.X448PrivateKey.generate()

    def sender_key(
        self,
        ephemeral_private_key: ec.EllipticCurvePrivateKey | x25519.X25519PrivateKey | x448.X448PrivateKey,
        *,
        alg: str,
        key_size: int,
        **headers: Any,
    ) -> BinaPy:
        """Compute a CEK for encryption of a message. This method is meant for usage by a sender.

        Args:
          ephemeral_private_key: the EPK to use for this key
          alg: the content encryption algorithm identifier
          key_size: the expected CEK size
          **headers: additional headers to include for CEK derivation

        Returns:
            the CEK for encryption by the sender

        """
        with self.public_key_required() as key:
            apu = BinaPy(headers.get("apu", b"")).decode_from("b64u")
            apv = BinaPy(headers.get("apv", b"")).decode_from("b64u")
            otherinfo = self.otherinfo(alg, apu, apv, key_size)
            cek = self.derive(
                private_key=ephemeral_private_key,
                public_key=key,
                otherinfo=otherinfo,
                key_size=key_size,
            )
            return cek

    def recipient_key(
        self,
        ephemeral_public_key: ec.EllipticCurvePublicKey | x25519.X25519PublicKey | x448.X448PublicKey,
        *,
        alg: str,
        key_size: int,
        **headers: Any,
    ) -> BinaPy:
        """Compute a shared key, for use by the recipient of an encrypted message.

        Args:
          ephemeral_public_key: the EPK, as received from sender
          alg: the content encryption algorithm identifier
          key_size: the CEK size
          **headers: additional headers as received from sender

        Returns:
            the CEK for decryption by the recipient

        """
        with self.private_key_required() as key:
            apu = BinaPy(headers.get("apu", b"")).decode_from("b64u")
            apv = BinaPy(headers.get("apv", b"")).decode_from("b64u")
            otherinfo = self.otherinfo(alg, apu, apv, key_size)
            cek = self.derive(
                private_key=key,
                public_key=ephemeral_public_key,
                otherinfo=otherinfo,
                key_size=key_size,
            )
            return cek

otherinfo classmethod

1
2
3
otherinfo(
    alg: str, apu: bytes, apv: bytes, key_size: int
) -> BinaPy

Build the "otherinfo" parameter for Concat KDF Hash.

Parameters:

Name Type Description Default
alg str

identifier for the encryption alg

required
apu bytes

Agreement PartyUInfo

required
apv bytes

Agreement PartyVInfo

required
key_size int

length of the generated key

required

Returns:

Type Description
BinaPy

the "otherinfo" value

Source code in jwskate/jwa/key_mgmt/ecdh.py
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
@classmethod
def otherinfo(cls, alg: str, apu: bytes, apv: bytes, key_size: int) -> BinaPy:
    """Build the "otherinfo" parameter for Concat KDF Hash.

    Args:
      alg: identifier for the encryption alg
      apu: Agreement PartyUInfo
      apv: Agreement PartyVInfo
      key_size: length of the generated key

    Returns:
        the "otherinfo" value

    """
    algorithm_id = BinaPy.from_int(len(alg), length=4) + BinaPy(alg)
    partyuinfo = BinaPy.from_int(len(apu), length=4) + apu
    partyvinfo = BinaPy.from_int(len(apv), length=4) + apv
    supppubinfo = BinaPy.from_int(key_size or key_size, length=4)
    otherinfo = b"".join((algorithm_id, partyuinfo, partyvinfo, supppubinfo))
    return BinaPy(otherinfo)

ecdh classmethod

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
ecdh(
    private_key: (
        ec.EllipticCurvePrivateKey
        | x25519.X25519PrivateKey
        | x448.X448PrivateKey
    ),
    public_key: (
        ec.EllipticCurvePublicKey
        | x25519.X25519PublicKey
        | x448.X448PublicKey
    ),
) -> BinaPy

Perform an Elliptic Curve Diffie-Hellman key exchange.

This derives a shared key between a sender and a receiver, based on a public and a private key from each side. ECDH exchange produces the same key with either a sender private key and a recipient public key, or the matching sender public key and recipient private key.

Parameters:

Name Type Description Default
private_key EllipticCurvePrivateKey | X25519PrivateKey | X448PrivateKey

a private EC key

required
public_key EllipticCurvePublicKey | X25519PublicKey | X448PublicKey

a public EC key

required

Returns:

Type Description
BinaPy

a shared key

Source code in jwskate/jwa/key_mgmt/ecdh.py
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
@classmethod
def ecdh(
    cls,
    private_key: ec.EllipticCurvePrivateKey | x25519.X25519PrivateKey | x448.X448PrivateKey,
    public_key: ec.EllipticCurvePublicKey | x25519.X25519PublicKey | x448.X448PublicKey,
) -> BinaPy:
    """Perform an Elliptic Curve Diffie-Hellman key exchange.

    This derives a shared key between a sender and a receiver, based on a public and a private key from each side.
    ECDH exchange produces the same key with either a sender private key and a recipient public key,
    or the matching sender public key and recipient private key.

    Args:
      private_key: a private EC key
      public_key: a public EC key

    Returns:
      a shared key

    """
    if isinstance(private_key, ec.EllipticCurvePrivateKey) and isinstance(public_key, ec.EllipticCurvePublicKey):
        shared_key = private_key.exchange(ec.ECDH(), public_key)
    elif isinstance(private_key, x25519.X25519PrivateKey) and isinstance(  # noqa: SIM114
        public_key,
        x25519.X25519PublicKey,
    ):
        shared_key = private_key.exchange(public_key)
    elif isinstance(private_key, x448.X448PrivateKey) and isinstance(public_key, x448.X448PublicKey):
        shared_key = private_key.exchange(public_key)
    else:
        msg = "Invalid or unsupported private/public key combination for ECDH"
        raise TypeError(
            msg,
            type(private_key),
            type(public_key),
        )
    return BinaPy(shared_key)

derive classmethod

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
derive(
    *,
    private_key: (
        ec.EllipticCurvePrivateKey
        | x25519.X25519PrivateKey
        | x448.X448PrivateKey
    ),
    public_key: (
        ec.EllipticCurvePublicKey
        | x25519.X25519PublicKey
        | x448.X448PublicKey
    ),
    otherinfo: bytes,
    key_size: int
) -> BinaPy

Derive a key using ECDH and Concat KDF Hash.

Parameters:

Name Type Description Default
private_key EllipticCurvePrivateKey | X25519PrivateKey | X448PrivateKey

the private key

required
public_key EllipticCurvePublicKey | X25519PublicKey | X448PublicKey

the public key

required
otherinfo bytes

the Concat KDF "otherinfo" parameter

required
key_size int

the expected CEK key size

required

Returns:

Type Description
BinaPy

the derived key

Source code in jwskate/jwa/key_mgmt/ecdh.py
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
@classmethod
def derive(
    cls,
    *,
    private_key: ec.EllipticCurvePrivateKey | x25519.X25519PrivateKey | x448.X448PrivateKey,
    public_key: ec.EllipticCurvePublicKey | x25519.X25519PublicKey | x448.X448PublicKey,
    otherinfo: bytes,
    key_size: int,
) -> BinaPy:
    """Derive a key using ECDH and Concat KDF Hash.

    Args:
      private_key: the private key
      public_key: the public key
      otherinfo: the Concat KDF "otherinfo" parameter
      key_size: the expected CEK key size

    Returns:
        the derived key

    """
    shared_key = cls.ecdh(private_key, public_key)
    ckdf = ConcatKDFHash(algorithm=hashes.SHA256(), length=key_size // 8, otherinfo=otherinfo)
    return BinaPy(ckdf.derive(shared_key))

generate_ephemeral_key

1
2
3
4
5
generate_ephemeral_key() -> (
    ec.EllipticCurvePrivateKey
    | x25519.X25519PrivateKey
    | x448.X448PrivateKey
)

Generate an ephemeral key that is suitable for use with this algorithm.

Returns:

Type Description
EllipticCurvePrivateKey | X25519PrivateKey | X448PrivateKey

a generated EllipticCurvePrivateKey, on the same curve as this algorithm key

Source code in jwskate/jwa/key_mgmt/ecdh.py
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
def generate_ephemeral_key(
    self,
) -> ec.EllipticCurvePrivateKey | x25519.X25519PrivateKey | x448.X448PrivateKey:
    """Generate an ephemeral key that is suitable for use with this algorithm.

    Returns:
        a generated EllipticCurvePrivateKey, on the same curve as this algorithm key

    """
    if isinstance(self.key, (ec.EllipticCurvePrivateKey, ec.EllipticCurvePublicKey)):
        return ec.generate_private_key(self.key.curve)
    elif isinstance(self.key, (x25519.X25519PrivateKey, x25519.X25519PublicKey)):
        return x25519.X25519PrivateKey.generate()
    elif isinstance(self.key, (x448.X448PublicKey, x448.X448PrivateKey)):
        return x448.X448PrivateKey.generate()

sender_key

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
sender_key(
    ephemeral_private_key: (
        ec.EllipticCurvePrivateKey
        | x25519.X25519PrivateKey
        | x448.X448PrivateKey
    ),
    *,
    alg: str,
    key_size: int,
    **headers: Any
) -> BinaPy

Compute a CEK for encryption of a message. This method is meant for usage by a sender.

Parameters:

Name Type Description Default
ephemeral_private_key EllipticCurvePrivateKey | X25519PrivateKey | X448PrivateKey

the EPK to use for this key

required
alg str

the content encryption algorithm identifier

required
key_size int

the expected CEK size

required
**headers Any

additional headers to include for CEK derivation

{}

Returns:

Type Description
BinaPy

the CEK for encryption by the sender

Source code in jwskate/jwa/key_mgmt/ecdh.py
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
173
174
175
def sender_key(
    self,
    ephemeral_private_key: ec.EllipticCurvePrivateKey | x25519.X25519PrivateKey | x448.X448PrivateKey,
    *,
    alg: str,
    key_size: int,
    **headers: Any,
) -> BinaPy:
    """Compute a CEK for encryption of a message. This method is meant for usage by a sender.

    Args:
      ephemeral_private_key: the EPK to use for this key
      alg: the content encryption algorithm identifier
      key_size: the expected CEK size
      **headers: additional headers to include for CEK derivation

    Returns:
        the CEK for encryption by the sender

    """
    with self.public_key_required() as key:
        apu = BinaPy(headers.get("apu", b"")).decode_from("b64u")
        apv = BinaPy(headers.get("apv", b"")).decode_from("b64u")
        otherinfo = self.otherinfo(alg, apu, apv, key_size)
        cek = self.derive(
            private_key=ephemeral_private_key,
            public_key=key,
            otherinfo=otherinfo,
            key_size=key_size,
        )
        return cek

recipient_key

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
recipient_key(
    ephemeral_public_key: (
        ec.EllipticCurvePublicKey
        | x25519.X25519PublicKey
        | x448.X448PublicKey
    ),
    *,
    alg: str,
    key_size: int,
    **headers: Any
) -> BinaPy

Compute a shared key, for use by the recipient of an encrypted message.

Parameters:

Name Type Description Default
ephemeral_public_key EllipticCurvePublicKey | X25519PublicKey | X448PublicKey

the EPK, as received from sender

required
alg str

the content encryption algorithm identifier

required
key_size int

the CEK size

required
**headers Any

additional headers as received from sender

{}

Returns:

Type Description
BinaPy

the CEK for decryption by the recipient

Source code in jwskate/jwa/key_mgmt/ecdh.py
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
def recipient_key(
    self,
    ephemeral_public_key: ec.EllipticCurvePublicKey | x25519.X25519PublicKey | x448.X448PublicKey,
    *,
    alg: str,
    key_size: int,
    **headers: Any,
) -> BinaPy:
    """Compute a shared key, for use by the recipient of an encrypted message.

    Args:
      ephemeral_public_key: the EPK, as received from sender
      alg: the content encryption algorithm identifier
      key_size: the CEK size
      **headers: additional headers as received from sender

    Returns:
        the CEK for decryption by the recipient

    """
    with self.private_key_required() as key:
        apu = BinaPy(headers.get("apu", b"")).decode_from("b64u")
        apv = BinaPy(headers.get("apv", b"")).decode_from("b64u")
        otherinfo = self.otherinfo(alg, apu, apv, key_size)
        cek = self.derive(
            private_key=key,
            public_key=ephemeral_public_key,
            otherinfo=otherinfo,
            key_size=key_size,
        )
        return cek

EcdhEs_A128KW

Bases: BaseEcdhEs_AesKw

ECDH-ES using Concat KDF and "A128KW" wrapping.

Source code in jwskate/jwa/key_mgmt/ecdh.py
256
257
258
259
260
261
class EcdhEs_A128KW(BaseEcdhEs_AesKw):  # noqa: N801
    """ECDH-ES using Concat KDF and "A128KW" wrapping."""

    name = "ECDH-ES+A128KW"
    description = __doc__
    kwalg = A128KW

EcdhEs_A192KW

Bases: BaseEcdhEs_AesKw

ECDH-ES using Concat KDF and "A192KW" wrapping.

Source code in jwskate/jwa/key_mgmt/ecdh.py
264
265
266
267
268
269
class EcdhEs_A192KW(BaseEcdhEs_AesKw):  # noqa: N801
    """ECDH-ES using Concat KDF and "A192KW" wrapping."""

    name = "ECDH-ES+A192KW"
    description = __doc__
    kwalg = A192KW

EcdhEs_A256KW

Bases: BaseEcdhEs_AesKw

ECDH-ES using Concat KDF and "A256KW" wrapping.

Source code in jwskate/jwa/key_mgmt/ecdh.py
272
273
274
275
276
277
class EcdhEs_A256KW(BaseEcdhEs_AesKw):  # noqa: N801
    """ECDH-ES using Concat KDF and "A256KW" wrapping."""

    name = "ECDH-ES+A256KW"
    description = __doc__
    kwalg = A256KW

Pbes2_HS256_A128KW

Bases: BasePbes2

PBES2 with HMAC SHA-256 and "A128KW" wrapping.

Source code in jwskate/jwa/key_mgmt/pbes2.py
111
112
113
114
115
116
117
class Pbes2_HS256_A128KW(BasePbes2):  # noqa: N801
    """PBES2 with HMAC SHA-256 and "A128KW" wrapping."""

    name = "PBES2-HS256+A128KW"
    description = __doc__
    kwalg = A128KW
    hash_alg = hashes.SHA256()

Pbes2_HS384_A192KW

Bases: BasePbes2

PBES2 with HMAC SHA-384 and "A192KW" wrapping.

Source code in jwskate/jwa/key_mgmt/pbes2.py
120
121
122
123
124
125
126
class Pbes2_HS384_A192KW(BasePbes2):  # noqa: N801
    """PBES2 with HMAC SHA-384 and "A192KW" wrapping."""

    name = "PBES2-HS384+A192KW"
    description = __doc__
    kwalg = A192KW
    hash_alg = hashes.SHA384()

Pbes2_HS512_A256KW

Bases: BasePbes2

PBES2 with HMAC SHA-512 and "A256KW" wrapping.

Source code in jwskate/jwa/key_mgmt/pbes2.py
129
130
131
132
133
134
135
class Pbes2_HS512_A256KW(BasePbes2):  # noqa: N801
    """PBES2 with HMAC SHA-512 and "A256KW" wrapping."""

    name = "PBES2-HS512+A256KW"
    description = __doc__
    kwalg = A256KW
    hash_alg = hashes.SHA512()

RsaEsOaep

Bases: BaseRsaKeyWrap

RSAES OAEP using default parameters.

Source code in jwskate/jwa/key_mgmt/rsa.py
84
85
86
87
88
89
90
91
92
93
94
class RsaEsOaep(BaseRsaKeyWrap):
    """RSAES OAEP using default parameters."""

    name = "RSA-OAEP"
    description = __doc__

    padding = padding.OAEP(
        mgf=padding.MGF1(algorithm=hashes.SHA1()),  # noqa: S303
        algorithm=hashes.SHA1(),  # noqa: S303
        label=None,
    )

RsaEsOaepSha256

Bases: BaseRsaKeyWrap

RSAES OAEP using SHA-256 and MGF1 with SHA-256.

Source code in jwskate/jwa/key_mgmt/rsa.py
 97
 98
 99
100
101
102
103
104
105
106
107
class RsaEsOaepSha256(BaseRsaKeyWrap):
    """RSAES OAEP using SHA-256 and MGF1 with SHA-256."""

    name = "RSA-OAEP-256"
    description = __doc__

    padding = padding.OAEP(
        mgf=padding.MGF1(algorithm=hashes.SHA256()),
        algorithm=hashes.SHA256(),
        label=None,
    )

RsaEsOaepSha384

Bases: BaseRsaKeyWrap

RSA-OAEP using SHA-384 and MGF1 with SHA-384.

Source code in jwskate/jwa/key_mgmt/rsa.py
110
111
112
113
114
115
116
117
118
119
120
class RsaEsOaepSha384(BaseRsaKeyWrap):
    """RSA-OAEP using SHA-384 and MGF1 with SHA-384."""

    name = "RSA-OAEP-384"
    description = __doc__

    padding = padding.OAEP(
        mgf=padding.MGF1(algorithm=hashes.SHA384()),
        algorithm=hashes.SHA384(),
        label=None,
    )

RsaEsOaepSha512

Bases: BaseRsaKeyWrap

RSA-OAEP using SHA-512 and MGF1 with SHA-512.

Source code in jwskate/jwa/key_mgmt/rsa.py
123
124
125
126
127
128
129
130
131
132
133
class RsaEsOaepSha512(BaseRsaKeyWrap):
    """RSA-OAEP using SHA-512 and MGF1 with SHA-512."""

    name = "RSA-OAEP-512"
    description = __doc__

    padding = padding.OAEP(
        mgf=padding.MGF1(algorithm=hashes.SHA512()),
        algorithm=hashes.SHA512(),
        label=None,
    )

RsaEsPcks1v1_5

Bases: BaseRsaKeyWrap

RSAES-PKCS1-v1_5.

Source code in jwskate/jwa/key_mgmt/rsa.py
74
75
76
77
78
79
80
81
class RsaEsPcks1v1_5(BaseRsaKeyWrap):  # noqa: N801
    """RSAES-PKCS1-v1_5."""

    name = "RSA1_5"
    description = __doc__
    read_only = True

    padding = padding.PKCS1v15()

OKPCurve dataclass

Represent an Octet Key Pair (OKP) Curve.

Source code in jwskate/jwa/okp.py
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
@dataclass
class OKPCurve:
    """Represent an Octet Key Pair (OKP) Curve."""

    name: str
    """Curve name as defined in [IANA JOSE](https://www.iana.org/assignments/jose/jose.xhtml#web-
    key-elliptic-curve).

    This name will appear in `crv` headers.
    """

    description: str
    """Curve description (human readable)."""

    cryptography_private_key_class: type[Any]
    """`cryptography` private key class."""

    cryptography_public_key_class: type[Any]
    """`cryptography` public key class."""

    use: str
    """Curve usage (`'sig'` or '`enc'`)."""

    key_size: int
    """Size of keys, in bytes."""

    instances: ClassVar[dict[str, OKPCurve]] = {}
    """Registry of subclasses, in a {name: instance} mapping."""

    def __post_init__(self) -> None:
        """Automatically registers subclasses in the instance registry."""
        self.instances[self.name] = self

name instance-attribute

1
name: str

Curve name as defined in IANA JOSE.

This name will appear in crv headers.

description instance-attribute

1
description: str

Curve description (human readable).

cryptography_private_key_class instance-attribute

1
cryptography_private_key_class: type[Any]

cryptography private key class.

cryptography_public_key_class instance-attribute

1
cryptography_public_key_class: type[Any]

cryptography public key class.

use instance-attribute

1
use: str

Curve usage ('sig' or 'enc').

key_size instance-attribute

1
key_size: int

Size of keys, in bytes.

instances class-attribute

1
instances: dict[str, OKPCurve] = {}

Registry of subclasses, in a {name: instance} mapping.

__post_init__

1
__post_init__() -> None

Automatically registers subclasses in the instance registry.

Source code in jwskate/jwa/okp.py
79
80
81
def __post_init__(self) -> None:
    """Automatically registers subclasses in the instance registry."""
    self.instances[self.name] = self

ES256

Bases: BaseECSignatureAlg

ECDSA using P-256 and SHA-256.

Source code in jwskate/jwa/signature/ec.py
86
87
88
89
90
91
92
class ES256(BaseECSignatureAlg):
    """ECDSA using P-256 and SHA-256."""

    name = "ES256"
    description = __doc__
    curve = P_256
    hashing_alg = hashes.SHA256()

ES256K

Bases: BaseECSignatureAlg

ECDSA using secp256k1 and SHA-256.

Source code in jwskate/jwa/signature/ec.py
113
114
115
116
117
118
119
class ES256K(BaseECSignatureAlg):
    """ECDSA using secp256k1 and SHA-256."""

    name = "ES256k"
    description = __doc__
    curve = secp256k1
    hashing_alg = hashes.SHA256()

ES384

Bases: BaseECSignatureAlg

ECDSA using P-384 and SHA-384.

Source code in jwskate/jwa/signature/ec.py
 95
 96
 97
 98
 99
100
101
class ES384(BaseECSignatureAlg):
    """ECDSA using P-384 and SHA-384."""

    name = "ES384"
    description = __doc__
    curve = P_384
    hashing_alg = hashes.SHA384()

ES512

Bases: BaseECSignatureAlg

ECDSA using P-521 and SHA-512.

Source code in jwskate/jwa/signature/ec.py
104
105
106
107
108
109
110
class ES512(BaseECSignatureAlg):
    """ECDSA using P-521 and SHA-512."""

    name = "ES512"
    description = __doc__
    curve = P_521
    hashing_alg = hashes.SHA512()

HS256

Bases: BaseHMACSigAlg

HMAC using SHA-256.

Source code in jwskate/jwa/signature/hmac.py
49
50
51
52
53
54
55
class HS256(BaseHMACSigAlg):
    """HMAC using SHA-256."""

    name = "HS256"
    description = __doc__
    hashing_alg = hashes.SHA256()
    min_key_size = 256

HS384

Bases: BaseHMACSigAlg

HMAC using SHA-384.

Source code in jwskate/jwa/signature/hmac.py
58
59
60
61
62
63
64
class HS384(BaseHMACSigAlg):
    """HMAC using SHA-384."""

    name = "HS384"
    description = __doc__
    hashing_alg = hashes.SHA384()
    min_key_size = 384

HS512

Bases: BaseHMACSigAlg

HMAC using SHA-512.

Source code in jwskate/jwa/signature/hmac.py
67
68
69
70
71
72
73
class HS512(BaseHMACSigAlg):
    """HMAC using SHA-512."""

    name = "HS512"
    description = __doc__
    hashing_alg = hashes.SHA512()
    min_key_size = 512

PS256

Bases: BaseRSASigAlg

RSASSA-PSS using SHA-256 and MGF1 with SHA-256.

Source code in jwskate/jwa/signature/rsa.py
111
112
113
114
115
116
117
class PS256(BaseRSASigAlg):
    """RSASSA-PSS using SHA-256 and MGF1 with SHA-256."""

    name = "PS256"
    description = __doc__
    hashing_alg = hashes.SHA256()
    padding_alg = padding.PSS(mgf=padding.MGF1(hashes.SHA256()), salt_length=256 // 8)

PS384

Bases: BaseRSASigAlg

RSASSA-PSS using SHA-384 and MGF1 with SHA-384.

Source code in jwskate/jwa/signature/rsa.py
120
121
122
123
124
125
126
class PS384(BaseRSASigAlg):
    """RSASSA-PSS using SHA-384 and MGF1 with SHA-384."""

    name = "PS384"
    description = __doc__
    hashing_alg = hashes.SHA384()
    padding_alg = padding.PSS(mgf=padding.MGF1(hashes.SHA384()), salt_length=384 // 8)

PS512

Bases: BaseRSASigAlg

RSASSA-PSS using SHA-512 and MGF1 with SHA-512.

Source code in jwskate/jwa/signature/rsa.py
129
130
131
132
133
134
135
class PS512(BaseRSASigAlg):
    """RSASSA-PSS using SHA-512 and MGF1 with SHA-512."""

    name = "PS512"
    description = __doc__
    hashing_alg = hashes.SHA512()
    padding_alg = padding.PSS(mgf=padding.MGF1(hashes.SHA512()), salt_length=512 // 8)

RS256

Bases: BaseRSASigAlg

RSASSA-PKCS1-v1_5 using SHA-256.

Source code in jwskate/jwa/signature/rsa.py
87
88
89
90
91
92
class RS256(BaseRSASigAlg):
    """RSASSA-PKCS1-v1_5 using SHA-256."""

    name = "RS256"
    description = __doc__
    hashing_alg = hashes.SHA256()

RS384

Bases: BaseRSASigAlg

RSASSA-PKCS1-v1_5 using SHA-384.

Source code in jwskate/jwa/signature/rsa.py
 95
 96
 97
 98
 99
100
class RS384(BaseRSASigAlg):
    """RSASSA-PKCS1-v1_5 using SHA-384."""

    name = "RS384"
    description = __doc__
    hashing_alg = hashes.SHA384()

RS512

Bases: BaseRSASigAlg

RSASSA-PKCS1-v1_5 using SHA-256.

Source code in jwskate/jwa/signature/rsa.py
103
104
105
106
107
108
class RS512(BaseRSASigAlg):
    """RSASSA-PKCS1-v1_5 using SHA-256."""

    name = "RS512"
    description = __doc__
    hashing_alg = hashes.SHA512()

BaseECSignatureAlg

Bases: BaseAsymmetricAlg[EllipticCurvePrivateKey, EllipticCurvePublicKey], BaseSignatureAlg

Base class for Elliptic Curve signature algorithms.

Source code in jwskate/jwa/signature/ec.py
17
18
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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
class BaseECSignatureAlg(
    BaseAsymmetricAlg[ec.EllipticCurvePrivateKey, ec.EllipticCurvePublicKey],
    BaseSignatureAlg,
):
    """Base class for Elliptic Curve signature algorithms."""

    curve: EllipticCurve
    public_key_class = ec.EllipticCurvePublicKey
    private_key_class = ec.EllipticCurvePrivateKey

    @override
    @classmethod
    def check_key(cls, key: ec.EllipticCurvePrivateKey | ec.EllipticCurvePublicKey) -> None:
        if key.curve.name != cls.curve.cryptography_curve.name:
            msg = f"This key is on curve {key.curve.name}. An EC key on curve {cls.curve.name} is expected."
            raise ValueError(msg)

    @classmethod
    @override
    def with_random_key(cls) -> Self:
        return cls(ec.generate_private_key(cls.curve.cryptography_curve))

    @override
    def sign(self, data: bytes | SupportsBytes) -> BinaPy:
        if not isinstance(data, bytes):
            data = bytes(data)

        with self.private_key_required() as key:
            dss_sig = key.sign(data, ec.ECDSA(self.hashing_alg))
            r, s = asymmetric.utils.decode_dss_signature(dss_sig)
            return BinaPy.from_int(r, length=self.curve.coordinate_size) + BinaPy.from_int(
                s, length=self.curve.coordinate_size
            )

    @override
    def verify(self, data: bytes | SupportsBytes, signature: bytes | SupportsBytes) -> bool:
        if not isinstance(data, bytes):
            data = bytes(data)

        if not isinstance(signature, bytes):
            signature = bytes(signature)

        with self.public_key_required() as key:
            if len(signature) != self.curve.coordinate_size * 2:
                msg = (
                    f"Invalid signature length {len(signature)} bytes, expected {self.curve.coordinate_size * 2} bytes"
                )
                raise ValueError(msg)

            r_bytes, s_bytes = (
                signature[: self.curve.coordinate_size],
                signature[self.curve.coordinate_size :],
            )
            r = int.from_bytes(r_bytes, "big", signed=False)
            s = int.from_bytes(s_bytes, "big", signed=False)
            dss_signature = asymmetric.utils.encode_dss_signature(r, s)

            try:
                key.verify(
                    dss_signature,
                    data,
                    ec.ECDSA(self.hashing_alg),
                )
            except exceptions.InvalidSignature:
                return False
            else:
                return True

BaseHMACSigAlg

Bases: BaseSymmetricAlg, BaseSignatureAlg

Base class for HMAC signature algorithms.

Source code in jwskate/jwa/signature/hmac.py
14
15
16
17
18
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
class BaseHMACSigAlg(BaseSymmetricAlg, BaseSignatureAlg):
    """Base class for HMAC signature algorithms."""

    mac: type[hmac.HMAC] = hmac.HMAC
    min_key_size: int

    @classmethod
    @override
    def with_random_key(cls) -> Self:
        return cls(BinaPy.random_bits(cls.min_key_size))

    @override
    def sign(self, data: bytes | SupportsBytes) -> BinaPy:
        if not isinstance(data, bytes):
            data = bytes(data)

        if self.read_only:
            raise NotImplementedError
        m = self.mac(self.key, self.hashing_alg)
        m.update(data)
        signature = m.finalize()
        return BinaPy(signature)

    @override
    def verify(self, data: bytes | SupportsBytes, signature: bytes | SupportsBytes) -> bool:
        if not isinstance(data, bytes):
            data = bytes(data)

        if not isinstance(signature, bytes):
            signature = bytes(signature)

        candidate_signature = self.sign(data)
        return candidate_signature == signature

BaseRSASigAlg

Bases: BaseAsymmetricAlg[RSAPrivateKey, RSAPublicKey], BaseSignatureAlg

Base class for RSA based signature algorithms.

Source code in jwskate/jwa/signature/rsa.py
16
17
18
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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
class BaseRSASigAlg(
    BaseAsymmetricAlg[asymmetric.rsa.RSAPrivateKey, asymmetric.rsa.RSAPublicKey],
    BaseSignatureAlg,
):
    """Base class for RSA based signature algorithms."""

    padding_alg: padding.AsymmetricPadding = padding.PKCS1v15()
    min_key_size: int = 2048

    private_key_class = asymmetric.rsa.RSAPrivateKey
    public_key_class = asymmetric.rsa.RSAPublicKey

    @classmethod
    @override
    def with_random_key(cls) -> Self:
        return cls(rsa.generate_private_key(public_exponent=65537, key_size=cls.min_key_size))

    def sign(self, data: bytes | SupportsBytes) -> BinaPy:
        """Sign arbitrary data.

        Args:
          data: the data to sign

        Returns:
            the generated signature

        Raises:
            NotImplementedError: for algorithms that are considered insecure, only signature verification is available
            PrivateKeyRequired: if the configured key is not private

        """
        if self.read_only:
            raise NotImplementedError

        if not isinstance(data, bytes):
            data = bytes(data)

        with self.private_key_required() as key:
            return BinaPy(key.sign(data, self.padding_alg, self.hashing_alg))

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

        Args:
          data: the data to verify
          signature: the signature

        Returns:
            `True` if the signature is valid, `False` otherwise

        """
        if not isinstance(data, bytes):
            data = bytes(data)

        if not isinstance(signature, bytes):
            signature = bytes(signature)

        with self.public_key_required() as key:
            try:
                key.verify(
                    signature,
                    data,
                    self.padding_alg,
                    self.hashing_alg,
                )
            except exceptions.InvalidSignature:
                return False
            else:
                return True

sign

1
sign(data: bytes | SupportsBytes) -> BinaPy

Sign arbitrary data.

Parameters:

Name Type Description Default
data bytes | SupportsBytes

the data to sign

required

Returns:

Type Description
BinaPy

the generated signature

Raises:

Type Description
NotImplementedError

for algorithms that are considered insecure, only signature verification is available

PrivateKeyRequired

if the configured key is not private

Source code in jwskate/jwa/signature/rsa.py
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
def sign(self, data: bytes | SupportsBytes) -> BinaPy:
    """Sign arbitrary data.

    Args:
      data: the data to sign

    Returns:
        the generated signature

    Raises:
        NotImplementedError: for algorithms that are considered insecure, only signature verification is available
        PrivateKeyRequired: if the configured key is not private

    """
    if self.read_only:
        raise NotImplementedError

    if not isinstance(data, bytes):
        data = bytes(data)

    with self.private_key_required() as key:
        return BinaPy(key.sign(data, self.padding_alg, self.hashing_alg))

verify

1
2
3
4
verify(
    data: bytes | SupportsBytes,
    signature: bytes | SupportsBytes,
) -> bool

Verify a signature against some data.

Parameters:

Name Type Description Default
data bytes | SupportsBytes

the data to verify

required
signature bytes | SupportsBytes

the signature

required

Returns:

Type Description
bool

True if the signature is valid, False otherwise

Source code in jwskate/jwa/signature/rsa.py
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
def verify(self, data: bytes | SupportsBytes, signature: bytes | SupportsBytes) -> bool:
    """Verify a signature against some data.

    Args:
      data: the data to verify
      signature: the signature

    Returns:
        `True` if the signature is valid, `False` otherwise

    """
    if not isinstance(data, bytes):
        data = bytes(data)

    if not isinstance(signature, bytes):
        signature = bytes(signature)

    with self.public_key_required() as key:
        try:
            key.verify(
                signature,
                data,
                self.padding_alg,
                self.hashing_alg,
            )
        except exceptions.InvalidSignature:
            return False
        else:
            return True

Ed448Dsa

Bases: BaseAsymmetricAlg[Ed448PrivateKey, Ed448PublicKey], BaseSignatureAlg

EdDSA signature algorithm with Ed25519 curve.

Source code in jwskate/jwa/signature/eddsa.py
76
77
78
79
80
81
82
83
84
85
86
87
88
89
class Ed448Dsa(
    BaseAsymmetricAlg[
        ed448.Ed448PrivateKey,
        ed448.Ed448PublicKey,
    ],
    BaseSignatureAlg,
):
    """EdDSA signature algorithm with Ed25519 curve."""

    description = __doc__
    hashing_alg = hashes.SHAKE256(114)

    private_key_class = ed448.Ed448PrivateKey
    public_key_class = ed448.Ed448PublicKey

Ed25519Dsa

Bases: BaseAsymmetricAlg[Ed25519PrivateKey, Ed25519PublicKey], BaseSignatureAlg

EdDSA signature algorithm with Ed25519 curve.

Source code in jwskate/jwa/signature/eddsa.py
60
61
62
63
64
65
66
67
68
69
70
71
72
73
class Ed25519Dsa(
    BaseAsymmetricAlg[
        ed25519.Ed25519PrivateKey,
        ed25519.Ed25519PublicKey,
    ],
    BaseSignatureAlg,
):
    """EdDSA signature algorithm with Ed25519 curve."""

    description = __doc__
    hashing_alg = hashes.SHA256()

    private_key_class = ed25519.Ed25519PrivateKey
    public_key_class = ed25519.Ed25519PublicKey

EdDsa

Bases: BaseAsymmetricAlg[Union[Ed25519PrivateKey, Ed448PrivateKey], Union[Ed25519PublicKey, Ed448PublicKey]], BaseSignatureAlg

EdDSA signature algorithms.

Source code in jwskate/jwa/signature/eddsa.py
16
17
18
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
48
49
50
51
52
53
54
55
56
57
class EdDsa(
    BaseAsymmetricAlg[
        Union[ed25519.Ed25519PrivateKey, ed448.Ed448PrivateKey],
        Union[ed25519.Ed25519PublicKey, ed448.Ed448PublicKey],
    ],
    BaseSignatureAlg,
):
    """EdDSA signature algorithms."""

    private_key_class = (ed25519.Ed25519PrivateKey, ed448.Ed448PrivateKey)
    public_key_class = (ed25519.Ed25519PublicKey, ed448.Ed448PublicKey)

    name = "EdDSA"
    description = __doc__

    @classmethod
    @override
    def with_random_key(cls) -> Self:
        return cls(ed25519.Ed25519PrivateKey.generate())

    @override
    def sign(self, data: bytes | SupportsBytes) -> BinaPy:
        if not isinstance(data, bytes):
            data = bytes(data)

        with self.private_key_required() as key:
            return BinaPy(key.sign(data))

    @override
    def verify(self, data: bytes | SupportsBytes, signature: bytes | SupportsBytes) -> bool:
        if not isinstance(data, bytes):
            data = bytes(data)
        if not isinstance(signature, bytes):
            signature = bytes(signature)

        with self.public_key_required() as key:
            try:
                key.verify(signature, data)
            except exceptions.InvalidSignature:
                return False
            else:
                return True