diff --git a/CHANGES.rst b/CHANGES.rst index 03dfaba..4394c1a 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,8 +4,9 @@ Changes Unreleased ---------- +- Make MAC key can be derived with ECDH. `#139 `__ - Add RawKey for key material. `#138 `__ -- Make MAC key can be used for KDF. `#137 `__ +- Make MAC key can be derived with HKDF. `#137 `__ - Remove COSEKeyInterface from RecipientInterface. `#137 `__ - Implement AESKeyWrap which has COSEKeyInterface. `#137 `__ - Add encode_key() to RecipientInterface. `#134 `__ diff --git a/README.md b/README.md index 4eb4cf6..56eae0b 100644 --- a/README.md +++ b/README.md @@ -53,8 +53,18 @@ See [Document](https://python-cwt.readthedocs.io/en/stable/) for details. - [COSE Usage Examples](#cose-usage-examples) - [COSE MAC0](#cose-mac0) - [COSE MAC](#cose-mac) + - [Direct Key Distribution](#direct-key-distribution-for-mac) + - [Direct Key with KDF](#direct-key-with-kdf-for-mac) + - [AES Key Wrap](#aes-key-wrap-for-mac) + - [Direct key Agreement](#direct-key-agreement-for-mac) + - [Key Agreement with Key Wrap](#key-agreement-with-key-wrap-for-mac) - [COSE Encrypt0](#cose-encrypt0) - [COSE Encrypt](#cose-encrypt) + - [Direct Key Distribution](#direct-key-distribution-for-encryption) + - [Direct Key with KDF](#direct-key-with-kdf-for-encryption) + - [AES Key Wrap](#aes-key-wrap-for-encryption) + - [Direct key Agreement](#direct-key-agreement-for-encryption) + - [Key Agreement with Key Wrap](#key-agreement-with-key-wrap-for-encryption) - [COSE Signature1](#cose-signature1) - [COSE Signature](#cose-signature) - [API Reference](#api-reference) @@ -432,7 +442,7 @@ from cwt import COSE, COSEKey mac_key = COSEKey.from_symmetric_key(alg="HS256", kid="01") ctx = COSE.new(alg_auto_inclusion=True, kid_auto_inclusion=True) encoded = ctx.encode_and_mac(b"Hello world!", mac_key) -decoded = ctx.decode(encoded, mac_key) +assert b"Hello world!" == ctx.decode(encoded, mac_key) ``` Following two samples are other ways of writing the above example: @@ -448,7 +458,7 @@ encoded = ctx.encode_and_mac( protected={"alg": "HS256"}, unprotected={"kid": "01"}, ) -decoded = ctx.decode(encoded, mac_key) +assert b"Hello world!" == ctx.decode(encoded, mac_key) ``` ```py @@ -462,23 +472,200 @@ encoded = ctx.encode_and_mac( protected={1: 5}, unprotected={4: b"01"}, ) -decoded = ctx.decode(encoded, mac_key) +assert b"Hello world!" == ctx.decode(encoded, mac_key) ``` ### COSE MAC -Create a COSE MAC message, verify and decode it as follows: +#### Direct Key Distribution for MAC + +The direct key distribution shares a MAC key between the sender and the recipient that is used directly. +The follwing example shows the simplest way to make a COSE MAC message, verify and decode it with the direct +key distribution method. ```py from cwt import COSE, COSEKey, Recipient -recipient = Recipient.from_jwk({"alg": "direct", "kid": "01"}) +# The sender makes a COSE MAC message as follows: mac_key = COSEKey.from_symmetric_key(alg="HS512", kid="01") +r = Recipient.from_jwk({"alg": "direct"}) +r.apply(mac_key) ctx = COSE.new() -encoded = ctx.encode_and_mac(b"Hello world!", mac_key, recipients=[recipient]) -decoded = ctx.decode(encoded, mac_key) +encoded = ctx.encode_and_mac(b"Hello world!", mac_key, recipients=[r]) + +# The recipient has the same MAC key and can verify and decode it: +assert b"Hello world!" == ctx.decode(encoded, mac_key) +``` + +#### Direct Key with KDF for MAC + + +```py +from secrets import token_bytes +from cwt import COSE, COSEKey, Recipient + +shared_material = token_bytes(32) +shared_key = COSEKey.from_symmetric_key(shared_material, kid="01") + +# The sender side: +r = Recipient.from_jwk( + { + "kty": "oct", + "alg": "direct+HKDF-SHA-256", + "salt": "aabbccddeeffgghh", + }, +) +mac_key = r.apply(shared_key, context={"alg": "HS256"}) +ctx = COSE.new(alg_auto_inclusion=True) +encoded = ctx.encode_and_mac( + b"Hello world!", + key=mac_key, + recipients=[r], +) + +# The recipient side: +assert b"Hello world!" == ctx.decode(encoded, shared_key, context={"alg": "HS256"}) +``` + +#### AES Key Wrap for MAC + +The AES key wrap algorithm can be used to wrap a MAC key as follows: + +```py +from cwt import COSE, COSEKey, Recipient + +# The sender side: +mac_key = COSEKey.from_symmetric_key(alg="HS512") +r = Recipient.from_jwk( + { + "alg": "A128KW", + "kid": "01", + "k": "hJtXIZ2uSN5kbQfbtTNWbg", # A shared wrapping key + }, +) +r.apply(mac_key) +ctx = COSE.new(alg_auto_inclusion=True) +encoded = ctx.encode_and_mac(b"Hello world!", key=mac_key, recipients=[r]) + +# The recipient side: +shared_key = COSEKey.from_jwk( + { + "kty": "oct", + "alg": "A128KW", + "kid": "01", + "k": "hJtXIZ2uSN5kbQfbtTNWbg", + }, +) +assert b"Hello world!" == ctx.decode(encoded, shared_key) +``` + +#### Direct Key Agreement for MAC + +The direct key agreement methods can be used to create a shared secret. A KDF (Key Distribution Function) is then +applied to the shared secret to derive a key to be used to protect the data. +The follwing example shows a simple way to make a COSE Encrypt message, verify and decode it with the direct key +agreement methods (``ECDH-ES+HKDF-256`` with various curves). + +```py +from cwt import COSE, COSEKey, Recipient + +# The sender side: +r = Recipient.from_jwk( + { + "kty": "EC", + "alg": "ECDH-ES+HKDF-256", + "crv": "P-256", + }, +) +# The following key is provided by the recipient in advance. +pub_key = COSEKey.from_jwk( + { + "kty": "EC", + "alg": "ECDH-ES+HKDF-256", + "kid": "01", + "crv": "P-256", + "x": "Ze2loSV3wrroKUN_4zhwGhCqo3Xhu1td4QjeQ5wIVR0", + "y": "HlLtdXARY_f55A3fnzQbPcm6hgr34Mp8p-nuzQCE0Zw", + } +) +mac_key = r.apply(recipient_key=pub_key, context={"alg": "HS256"}) +ctx = COSE.new(alg_auto_inclusion=True) +encoded = ctx.encode_and_mac( + b"Hello world!", + key=mac_key, + recipients=[r], +) + +# The recipient side: +# The following key is the private key of the above pub_key. +priv_key = COSEKey.from_jwk( + { + "kty": "EC", + "alg": "ECDH-ES+HKDF-256", + "kid": "01", + "crv": "P-256", + "x": "Ze2loSV3wrroKUN_4zhwGhCqo3Xhu1td4QjeQ5wIVR0", + "y": "HlLtdXARY_f55A3fnzQbPcm6hgr34Mp8p-nuzQCE0Zw", + "d": "r_kHyZ-a06rmxM3yESK84r1otSg-aQcVStkRhA-iCM8", + } +) +# The enc_key will be derived in decode() with priv_key and +# the sender's public key which is conveyed as the recipient +# information structure in the COSE Encrypt message (encoded). +assert b"Hello world!" == ctx.decode(encoded, priv_key, context={"alg": "HS256"}) +``` + +#### Key Agreement with Key Wrap for MAC + + +```py +from cwt import COSE, COSEKey, Recipient + +# The sender side: +mac_key = COSEKey.from_symmetric_key(alg="HS256") +r = Recipient.from_jwk( + { + "kty": "EC", + "crv": "P-256", + "alg": "ECDH-SS+A128KW", + "x": "7cvYCcdU22WCwW1tZXR8iuzJLWGcd46xfxO1XJs-SPU", + "y": "DzhJXgz9RI6TseNmwEfLoNVns8UmvONsPzQDop2dKoo", + "d": "Uqr4fay_qYQykwcNCB2efj_NFaQRRQ-6fHZm763jt5w", + } +) +pub_key = COSEKey.from_jwk( + { + "kty": "EC", + "crv": "P-256", + "kid": "meriadoc.brandybuck@buckland.example", + "x": "Ze2loSV3wrroKUN_4zhwGhCqo3Xhu1td4QjeQ5wIVR0", + "y": "HlLtdXARY_f55A3fnzQbPcm6hgr34Mp8p-nuzQCE0Zw", + } +) +r.apply(mac_key, recipient_key=pub_key, context={"alg": "HS256"}) +ctx = COSE.new(alg_auto_inclusion=True) +encoded = ctx.encode_and_mac( + b"Hello world!", + key=mac_key, + recipients=[r], +) + +# The recipient side: +priv_key = COSEKey.from_jwk( + { + "kty": "EC", + "crv": "P-256", + "alg": "ECDH-SS+A128KW", + "kid": "meriadoc.brandybuck@buckland.example", + "x": "Ze2loSV3wrroKUN_4zhwGhCqo3Xhu1td4QjeQ5wIVR0", + "y": "HlLtdXARY_f55A3fnzQbPcm6hgr34Mp8p-nuzQCE0Zw", + "d": "r_kHyZ-a06rmxM3yESK84r1otSg-aQcVStkRhA-iCM8", + } +) +assert b"Hello world!" == ctx.decode(encoded, priv_key, context={"alg": "HS256"}) ``` + ### COSE Encrypt0 Create a COSE Encrypt0 message, verify and decode it as follows: @@ -494,20 +681,192 @@ decoded = ctx.decode(encoded, enc_key) ### COSE Encrypt -Create a COSE Encrypt message, verify and decode it as follows: +#### Direct Key Distribution for encryption + +The direct key distribution shares a MAC key between the sender and the recipient that is used directly. +The follwing example shows the simplest way to make a COSE MAC message, verify and decode it with the direct +key distribution method. ```py from cwt import COSE, COSEKey, Recipient -recipient = Recipient.from_jwk({"alg": "direct", "kid": "01"}) enc_key = COSEKey.from_symmetric_key(alg="ChaCha20/Poly1305", kid="01") + +# The sender side: +nonce = enc_key.generate_nonce() +r = Recipient.from_jwk({"alg": "direct"}) +r.apply(enc_key) ctx = COSE.new() encoded = ctx.encode_and_encrypt( b"Hello world!", enc_key, - recipients=[recipient], + nonce=nonce, + recipients=[r], ) -decoded = ctx.decode(encoded, enc_key) + +# The recipient side: +assert b"Hello world!" == ctx.decode(encoded, enc_key) +``` + +#### Direct Key with KDF for encryption + + +```py +from cwt import COSE, COSEKey, Recipient + +shared_material = token_bytes(32) +shared_key = COSEKey.from_symmetric_key(shared_material, kid="01") + +# The sender side: +r = Recipient.from_jwk( + { + "kty": "oct", + "alg": "direct+HKDF-SHA-256", + "salt": "aabbccddeeffgghh", + }, +) +enc_key = r.apply(shared_key, context={"alg": "A256GCM"}) +ctx = COSE.new(alg_auto_inclusion=True) +encoded = ctx.encode_and_encrypt( + b"Hello world!", + key=enc_key, + recipients=[r], +) +# The recipient side: +assert b"Hello world!" == ctx.decode(encoded, shared_key, context={"alg": "A256GCM"}) +``` + +#### AES Key Wrap for encryption + +The AES key wrap algorithm can be used to wrap a MAC key as follows: + +```py +from cwt import COSE, COSEKey, Recipient + +# The sender side: +r = Recipient.from_jwk( + { + "kty": "oct", + "alg": "A128KW", + "kid": "01", + "k": "hJtXIZ2uSN5kbQfbtTNWbg", # A shared wrapping key + }, +) +enc_key = COSEKey.from_symmetric_key(alg="ChaCha20/Poly1305") +r.apply(enc_key) +ctx = COSE.new(alg_auto_inclusion=True) +encoded = ctx.encode_and_encrypt(b"Hello world!", key=enc_key, recipients=[r]) + +# The recipient side: +shared_key = COSEKey.from_jwk( + { + "kty": "oct", + "alg": "A128KW", + "kid": "01", + "k": "hJtXIZ2uSN5kbQfbtTNWbg", + }, +) +assert b"Hello world!" == ctx.decode(encoded, shared_key) +``` + +#### Direct Key Agreement for encryption + +The direct key agreement methods can be used to create a shared secret. A KDF (Key Distribution Function) is then +applied to the shared secret to derive a key to be used to protect the data. +The follwing example shows a simple way to make a COSE Encrypt message, verify and decode it with the direct key +agreement methods (``ECDH-ES+HKDF-256`` with various curves). + +```py +from cwt import COSE, COSEKey, Recipient + +# The sender side: +r = Recipient.from_jwk( + { + "kty": "OKP", + "alg": "ECDH-ES+HKDF-256", + "crv": "X25519", + }, +) +pub_key = COSEKey.from_jwk( + { + "kty": "OKP", + "alg": "ECDH-ES+HKDF-256", + "kid": "01", + "crv": "X25519", + "x": "y3wJq3uXPHeoCO4FubvTc7VcBuqpvUrSvU6ZMbHDTCI", + } +) +enc_key = r.apply(recipient_key=pub_key, context={"alg": "A128GCM"}) +ctx = COSE.new(alg_auto_inclusion=True) +encoded = ctx.encode_and_encrypt( + b"Hello world!", + key=enc_key, + recipients=[r], +) + +# The recipient side: +priv_key = COSEKey.from_jwk( + { + "kty": "OKP", + "alg": "ECDH-ES+HKDF-256", + "kid": "01", + "crv": "X25519", + "x": "y3wJq3uXPHeoCO4FubvTc7VcBuqpvUrSvU6ZMbHDTCI", + "d": "vsJ1oX5NNi0IGdwGldiac75r-Utmq3Jq4LGv48Q_Qc4", + } +) +assert b"Hello world!" == ctx.decode(encoded, priv_key, context={"alg": "A128GCM"}) +``` + +#### Key Agreement with Key Wrap for encryption + +```py +from cwt import COSE, COSEKey, Recipient + +# The sender side: +enc_key = COSEKey.from_symmetric_key(alg="A128GCM") +nonce = enc_key.generate_nonce() +r = Recipient.from_jwk( + { + "kty": "EC", + "crv": "P-256", + "alg": "ECDH-SS+A128KW", + "x": "7cvYCcdU22WCwW1tZXR8iuzJLWGcd46xfxO1XJs-SPU", + "y": "DzhJXgz9RI6TseNmwEfLoNVns8UmvONsPzQDop2dKoo", + "d": "Uqr4fay_qYQykwcNCB2efj_NFaQRRQ-6fHZm763jt5w", + } +) +pub_key = COSEKey.from_jwk( + { + "kty": "EC", + "crv": "P-256", + "kid": "meriadoc.brandybuck@buckland.example", + "x": "Ze2loSV3wrroKUN_4zhwGhCqo3Xhu1td4QjeQ5wIVR0", + "y": "HlLtdXARY_f55A3fnzQbPcm6hgr34Mp8p-nuzQCE0Zw", + } +) +r.apply(enc_key, recipient_key=pub_key, context={"alg": "A128GCM"}) +ctx = COSE.new(alg_auto_inclusion=True) +encoded = ctx.encode_and_encrypt( + b"Hello world!", + key=enc_key, + nonce=nonce, + recipients=[r], +) + +# The recipient side: +priv_key = COSEKey.from_jwk( + { + "kty": "EC", + "crv": "P-256", + "alg": "ECDH-SS+A128KW", + "kid": "meriadoc.brandybuck@buckland.example", + "x": "Ze2loSV3wrroKUN_4zhwGhCqo3Xhu1td4QjeQ5wIVR0", + "y": "HlLtdXARY_f55A3fnzQbPcm6hgr34Mp8p-nuzQCE0Zw", + "d": "r_kHyZ-a06rmxM3yESK84r1otSg-aQcVStkRhA-iCM8", + } +) +assert b"Hello world!" == ctx.decode(encoded, priv_key, context={"alg": "A128GCM"}) ``` ### COSE Signature1 @@ -515,21 +874,37 @@ decoded = ctx.decode(encoded, enc_key) Create a COSE Signature1 message, verify and decode it as follows: ```py -from cwt import COSE, COSEKey +from cwt import COSE, COSEKey, Signer + +# The sender side: +signer = Signer.new( + cose_key=COSEKey.from_jwk( + { + "kty": "EC", + "kid": "01", + "crv": "P-256", + "x": "usWxHK2PmfnHKwXPS54m0kTcGJ90UiglWiGahtagnv8", + "y": "IBOL-C3BttVivg-lSreASjpkttcsz-1rb7btKLv8EX4", + "d": "V8kgd2ZBRuh2dgyVINBUqpPDr7BOMGcF22CQMIUHtNM", + } + ), + protected={"alg": "ES256"}, + unprotected={"kid": "01"}, +) +ctx = COSE.new() +encoded = ctx.encode_and_sign(b"Hello world!", signers=[signer]) -sig_key = COSEKey.from_jwk( +# The recipient side: +pub_key = COSEKey.from_jwk( { "kty": "EC", "kid": "01", "crv": "P-256", "x": "usWxHK2PmfnHKwXPS54m0kTcGJ90UiglWiGahtagnv8", "y": "IBOL-C3BttVivg-lSreASjpkttcsz-1rb7btKLv8EX4", - "d": "V8kgd2ZBRuh2dgyVINBUqpPDr7BOMGcF22CQMIUHtNM", } ) -ctx = COSE.new(alg_auto_inclusion=True, kid_auto_inclusion=True) -encoded = ctx.encode_and_sign(b"Hello world!", sig_key) -decoded = ctx.decode(encoded, sig_key) +assert b"Hello world!" == ctx.decode(encoded, pub_key) ``` ### COSE Signature @@ -537,21 +912,37 @@ decoded = ctx.decode(encoded, sig_key) Create a COSE Signature message, verify and decode it as follows: ```py -from cwt import COSE, Signer +from cwt import COSE, COSEKey, Signer + +# The sender side: +signer = Signer.new( + cose_key=COSEKey.from_jwk( + { + "kty": "EC", + "kid": "01", + "crv": "P-256", + "x": "usWxHK2PmfnHKwXPS54m0kTcGJ90UiglWiGahtagnv8", + "y": "IBOL-C3BttVivg-lSreASjpkttcsz-1rb7btKLv8EX4", + "d": "V8kgd2ZBRuh2dgyVINBUqpPDr7BOMGcF22CQMIUHtNM", + } + ), + protected={1: -7}, + unprotected={4: b"01"}, +) +ctx = COSE.new() +encoded = ctx.encode_and_sign(b"Hello world!", signers=[signer]) -signer = Signer.from_jwk( +# The recipient side: +pub_key = COSEKey.from_jwk( { "kty": "EC", "kid": "01", "crv": "P-256", "x": "usWxHK2PmfnHKwXPS54m0kTcGJ90UiglWiGahtagnv8", "y": "IBOL-C3BttVivg-lSreASjpkttcsz-1rb7btKLv8EX4", - "d": "V8kgd2ZBRuh2dgyVINBUqpPDr7BOMGcF22CQMIUHtNM", - }, + } ) -ctx = COSE.new() -encoded = ctx.encode_and_sign(b"Hello world!", signers=[signer]) -decoded = ctx.decode(encoded, signer.cose_key) +assert b"Hello world!" == ctx.decode(encoded, pub_key) ``` ## API Reference diff --git a/cwt/algs/ec2.py b/cwt/algs/ec2.py index 20e626a..d877e03 100644 --- a/cwt/algs/ec2.py +++ b/cwt/algs/ec2.py @@ -24,7 +24,7 @@ from ..cose_key_interface import COSEKeyInterface from ..exceptions import EncodeError, VerifyError from ..utils import i2osp, os2ip, to_cis -from .symmetric import AESCCMKey, AESGCMKey, ChaCha20Key +from .symmetric import AESCCMKey, AESGCMKey, ChaCha20Key, HMACKey class EC2Key(COSEKeyInterface): @@ -328,6 +328,8 @@ def derive_key( } if cose_key[3] in [1, 2, 3]: return AESGCMKey(cose_key) + if cose_key[3] in [4, 5, 6, 7]: + return HMACKey(cose_key) if cose_key[3] in [10, 11, 12, 13, 30, 31, 32, 33]: return AESCCMKey(cose_key) # cose_key[3] == 24: diff --git a/cwt/algs/okp.py b/cwt/algs/okp.py index ac1c9ee..06fcea2 100644 --- a/cwt/algs/okp.py +++ b/cwt/algs/okp.py @@ -34,7 +34,7 @@ from ..cose_key_interface import COSEKeyInterface from ..exceptions import EncodeError, VerifyError from ..utils import to_cis -from .symmetric import AESCCMKey, AESGCMKey, ChaCha20Key +from .symmetric import AESCCMKey, AESGCMKey, ChaCha20Key, HMACKey class OKPKey(COSEKeyInterface): @@ -339,6 +339,8 @@ def derive_key( } if cose_key[3] in [1, 2, 3]: return AESGCMKey(cose_key) + if cose_key[3] in [4, 5, 6, 7]: + return HMACKey(cose_key) if cose_key[3] in [10, 11, 12, 13, 30, 31, 32, 33]: return AESCCMKey(cose_key) # cose_key[3] == 24: diff --git a/docs/cose_usage.rst b/docs/cose_usage.rst index c4be5b1..c045af5 100644 --- a/docs/cose_usage.rst +++ b/docs/cose_usage.rst @@ -81,10 +81,11 @@ key distribution method. from cwt import COSE, COSEKey, Recipient # The sender makes a COSE MAC message as follows: - recipient = Recipient.from_jwk({"alg": "direct", "kid": "01"}) mac_key = COSEKey.from_symmetric_key(alg="HS512", kid="01") + r = Recipient.from_jwk({"alg": "direct"}) + r.apply(mac_key) ctx = COSE.new() - encoded = ctx.encode_and_mac(b"Hello world!", mac_key, recipients=[recipient]) + encoded = ctx.encode_and_mac(b"Hello world!", mac_key, recipients=[r]) # The recipient has the same MAC key and can verify and decode it: assert b"Hello world!" == ctx.decode(encoded, mac_key) @@ -97,10 +98,11 @@ Following samples are other ways of writing the above sample: # The sender side: # In contrast to from_jwk(), new() is low-level constructor. - recipient = Recipient.new(unprotected={"alg": "direct", "kid": "01"}) mac_key = COSEKey.from_symmetric_key(alg="HS512", kid="01") + r = Recipient.new(unprotected={"alg": "direct"}) + r.apply(mac_key) ctx = COSE.new() - encoded = ctx.encode_and_mac(b"Hello world!", mac_key, recipients=[recipient]) + encoded = ctx.encode_and_mac(b"Hello world!", mac_key, recipients=[r]) # The recipient side: assert b"Hello world!" == ctx.decode(encoded, mac_key) @@ -111,14 +113,45 @@ Following samples are other ways of writing the above sample: # The sender side: # new() can accept following raw COSE header parameters. - recipient = Recipient.new(unprotected={1: 7, 4: b"01"}) mac_key = COSEKey.from_symmetric_key(alg="HS512", kid="01") + r = Recipient.new(unprotected={1: 7}) + r.apply(mac_key) ctx = COSE.new() - encoded = ctx.encode_and_mac(b"Hello world!", mac_key, recipients=[recipient]) + encoded = ctx.encode_and_mac(b"Hello world!", mac_key, recipients=[r]) # The recipient side: assert b"Hello world!" == ctx.decode(encoded, mac_key) +Direct Key with KDF +------------------- + +.. code-block:: python + + from secrets import token_bytes + from cwt import COSE, COSEKey, Recipient + + shared_material = token_bytes(32) + shared_key = COSEKey.from_symmetric_key(shared_material, kid="01") + + # The sender side: + r = Recipient.from_jwk( + { + "kty": "oct", + "alg": "direct+HKDF-SHA-256", + "salt": "aabbccddeeffgghh", + }, + ) + mac_key = r.apply(shared_key, context={"alg": "HS256"}) + ctx = COSE.new(alg_auto_inclusion=True) + encoded = ctx.encode_and_mac( + b"Hello world!", + key=mac_key, + recipients=[r], + ) + + # The recipient side: + assert b"Hello world!" == ctx.decode(encoded, shared_key, context={"alg": "HS256"}) + AES Key Wrap ------------ @@ -130,19 +163,223 @@ The AES key wrap algorithm can be used to wrap a MAC key as follows: # The sender side: mac_key = COSEKey.from_symmetric_key(alg="HS512") - recipient = Recipient.from_jwk( + r = Recipient.from_jwk( + { + "alg": "A128KW", + "kid": "01", + "k": "hJtXIZ2uSN5kbQfbtTNWbg", # A shared wrapping key + }, + ) + r.apply(mac_key) + ctx = COSE.new(alg_auto_inclusion=True) + encoded = ctx.encode_and_mac(b"Hello world!", key=mac_key, recipients=[r]) + + # The recipient side: + shared_key = COSEKey.from_jwk( { + "kty": "oct", "alg": "A128KW", - "kid": "our-secret", + "kid": "01", "k": "hJtXIZ2uSN5kbQfbtTNWbg", }, ) - recipient.wrap_key(mac_key.key) + assert b"Hello world!" == ctx.decode(encoded, shared_key) + +Direct Key Agreement +-------------------- + +The direct key agreement methods can be used to create a shared secret. A KDF (Key Distribution Function) is then +applied to the shared secret to derive a key to be used to protect the data. +The follwing example shows a simple way to make a COSE Encrypt message, verify and decode it with the direct key +agreement methods (``ECDH-ES+HKDF-256`` with various curves). + +.. code-block:: python + + from cwt import COSE, COSEKey, Recipient + + # The sender side: + r = Recipient.from_jwk( + { + "kty": "EC", + "alg": "ECDH-ES+HKDF-256", + "crv": "P-256", + }, + ) + # The following key is provided by the recipient in advance. + pub_key = COSEKey.from_jwk( + { + "kty": "EC", + "alg": "ECDH-ES+HKDF-256", + "kid": "01", + "crv": "P-256", + "x": "Ze2loSV3wrroKUN_4zhwGhCqo3Xhu1td4QjeQ5wIVR0", + "y": "HlLtdXARY_f55A3fnzQbPcm6hgr34Mp8p-nuzQCE0Zw", + } + ) + mac_key = r.apply(recipient_key=pub_key, context={"alg": "HS256"}) ctx = COSE.new(alg_auto_inclusion=True) - encoded = ctx.encode_and_mac(b"Hello world!", key=mac_key, recipients=[recipient]) + encoded = ctx.encode_and_mac( + b"Hello world!", + key=mac_key, + recipients=[r], + ) # The recipient side: - assert b"Hello world!" == ctx.decode(encoded, recipient) + # The following key is the private key of the above pub_key. + priv_key = COSEKey.from_jwk( + { + "kty": "EC", + "alg": "ECDH-ES+HKDF-256", + "kid": "01", + "crv": "P-256", + "x": "Ze2loSV3wrroKUN_4zhwGhCqo3Xhu1td4QjeQ5wIVR0", + "y": "HlLtdXARY_f55A3fnzQbPcm6hgr34Mp8p-nuzQCE0Zw", + "d": "r_kHyZ-a06rmxM3yESK84r1otSg-aQcVStkRhA-iCM8", + } + ) + # The enc_key will be derived in decode() with priv_key and + # the sender's public key which is conveyed as the recipient + # information structure in the COSE Encrypt message (encoded). + assert b"Hello world!" == ctx.decode(encoded, priv_key, context={"alg": "HS256"}) + +You can use other curves (``P-384``, ``P-521``, ``X25519``, ``X448``) instead of ``P-256``: + +In case of ``X25519``: + +.. code-block:: python + + from cwt import COSE, COSEKey, Recipient + + # The sender side: + r = Recipient.from_jwk( + { + "kty": "OKP", + "alg": "ECDH-ES+HKDF-256", + "crv": "X25519", + }, + ) + pub_key = COSEKey.from_jwk( + { + "kty": "OKP", + "alg": "ECDH-ES+HKDF-256", + "kid": "01", + "crv": "X25519", + "x": "y3wJq3uXPHeoCO4FubvTc7VcBuqpvUrSvU6ZMbHDTCI", + } + ) + mac_key = r.apply(recipient_key=pub_key, context={"alg": "HS256"}) + ctx = COSE.new(alg_auto_inclusion=True) + encoded = ctx.encode_and_mac( + b"Hello world!", + key=mac_key, + recipients=[r], + ) + + # The recipient side: + priv_key = COSEKey.from_jwk( + { + "kty": "OKP", + "alg": "ECDH-ES+HKDF-256", + "kid": "01", + "crv": "X25519", + "x": "y3wJq3uXPHeoCO4FubvTc7VcBuqpvUrSvU6ZMbHDTCI", + "d": "vsJ1oX5NNi0IGdwGldiac75r-Utmq3Jq4LGv48Q_Qc4", + } + ) + assert b"Hello world!" == ctx.decode(encoded, priv_key, context={"alg": "HS256"}) + +In case of ``X448``: + +.. code-block:: python + + from cwt import COSE, COSEKey, Recipient + + r = Recipient.from_jwk( + { + "kty": "OKP", + "alg": "ECDH-ES+HKDF-256", + "crv": "X448", + }, + ) + pub_key = COSEKey.from_jwk( + { + "kty": "OKP", + "alg": "ECDH-ES+HKDF-256", + "kid": "01", + "crv": "X448", + "x": "IkLmc0klvEMXYneHMKAB6ePohryAwAPVe2pRSffIDY6NrjeYNWVX5J-fG4NV2OoU77C88A0mvxI", + } + ) + mac_key = r.apply(recipient_key=pub_key, context={"alg": "HS256"}) + ctx = COSE.new(alg_auto_inclusion=True) + encoded = ctx.encode_and_mac( + b"Hello world!", + key=mac_key, + recipients=[r], + ) + priv_key = COSEKey.from_jwk( + { + "kty": "OKP", + "alg": "ECDH-ES+HKDF-256", + "kid": "01", + "crv": "X448", + "x": "IkLmc0klvEMXYneHMKAB6ePohryAwAPVe2pRSffIDY6NrjeYNWVX5J-fG4NV2OoU77C88A0mvxI", + "d": "rJJRG3nshyCtd9CgXld8aNaB9YXKR0UOi7zj7hApg9YH4XdBO0G8NcAFNz_uPH2GnCZVcSDgV5c", + } + ) + assert b"Hello world!" == ctx.decode(encoded, priv_key, context={"alg": "HS256"}) + + +Key Agreement with Key Wrap +--------------------------- + +.. code-block:: python + + from cwt import COSE, COSEKey, Recipient + + # The sender side: + mac_key = COSEKey.from_symmetric_key(alg="HS256") + r = Recipient.from_jwk( + { + "kty": "EC", + "crv": "P-256", + "alg": "ECDH-SS+A128KW", + "x": "7cvYCcdU22WCwW1tZXR8iuzJLWGcd46xfxO1XJs-SPU", + "y": "DzhJXgz9RI6TseNmwEfLoNVns8UmvONsPzQDop2dKoo", + "d": "Uqr4fay_qYQykwcNCB2efj_NFaQRRQ-6fHZm763jt5w", + } + ) + pub_key = COSEKey.from_jwk( + { + "kty": "EC", + "crv": "P-256", + "kid": "meriadoc.brandybuck@buckland.example", + "x": "Ze2loSV3wrroKUN_4zhwGhCqo3Xhu1td4QjeQ5wIVR0", + "y": "HlLtdXARY_f55A3fnzQbPcm6hgr34Mp8p-nuzQCE0Zw", + } + ) + r.apply(mac_key, recipient_key=pub_key, context={"alg": "HS256"}) + ctx = COSE.new(alg_auto_inclusion=True) + encoded = ctx.encode_and_mac( + b"Hello world!", + key=mac_key, + recipients=[r], + ) + + # The recipient side: + priv_key = COSEKey.from_jwk( + { + "kty": "EC", + "crv": "P-256", + "alg": "ECDH-SS+A128KW", + "kid": "meriadoc.brandybuck@buckland.example", + "x": "Ze2loSV3wrroKUN_4zhwGhCqo3Xhu1td4QjeQ5wIVR0", + "y": "HlLtdXARY_f55A3fnzQbPcm6hgr34Mp8p-nuzQCE0Zw", + "d": "r_kHyZ-a06rmxM3yESK84r1otSg-aQcVStkRhA-iCM8", + } + ) + assert b"Hello world!" == ctx.decode(encoded, priv_key, context={"alg": "HS256"}) + COSE Encrypt0 ============= @@ -153,10 +390,12 @@ Create a COSE Encrypt0 message, verify and decode it as follows: from cwt import COSE, COSEKey - # The sender side: enc_key = COSEKey.from_symmetric_key(alg="ChaCha20/Poly1305", kid="01") + + # The sender side: + nonce = enc_key.generate_nonce() ctx = COSE.new(alg_auto_inclusion=True, kid_auto_inclusion=True) - encoded = ctx.encode_and_encrypt(b"Hello world!", enc_key) + encoded = ctx.encode_and_encrypt(b"Hello world!", enc_key, nonce=nonce) # The recipient side: assert b"Hello world!" == ctx.decode(encoded, enc_key) @@ -169,8 +408,10 @@ Following two samples are other ways of writing the above example: from cwt import COSE, COSEKey - # The sender side: enc_key = COSEKey.from_symmetric_key(alg="ChaCha20/Poly1305", kid="01") + + # The sender side: + nonce = enc_key.generate_nonce() ctx = COSE.new() encoded = ctx.encode_and_encrypt( b"Hello world!", @@ -187,8 +428,10 @@ Following two samples are other ways of writing the above example: from cwt import COSE, COSEKey - # The sender side: enc_key = COSEKey.from_symmetric_key(alg="ChaCha20/Poly1305", kid="01") + + # The sender side: + nonce = enc_key.generate_nonce() ctx = COSE.new() encoded = ctx.encode_and_encrypt( b"Hello world!", @@ -215,24 +458,90 @@ key distribution method. from cwt import COSE, COSEKey, Recipient - # The sender side: - recipient = Recipient.from_jwk({"alg": "direct", "kid": "01"}) enc_key = COSEKey.from_symmetric_key(alg="ChaCha20/Poly1305", kid="01") + + # The sender side: + nonce = enc_key.generate_nonce() + r = Recipient.from_jwk({"alg": "direct"}) + r.apply(enc_key) ctx = COSE.new() encoded = ctx.encode_and_encrypt( b"Hello world!", enc_key, - recipients=[recipient], + nonce=nonce, + recipients=[r], ) # The recipient side: assert b"Hello world!" == ctx.decode(encoded, enc_key) +Direct Key with KDF +------------------- + +.. code-block:: python + + from cwt import COSE, COSEKey, Recipient + + shared_material = token_bytes(32) + shared_key = COSEKey.from_symmetric_key(shared_material, kid="01") + + # The sender side: + r = Recipient.from_jwk( + { + "kty": "oct", + "alg": "direct+HKDF-SHA-256", + "salt": "aabbccddeeffgghh", + }, + ) + enc_key = r.apply(shared_key, context={"alg": "A256GCM"}) + ctx = COSE.new(alg_auto_inclusion=True) + encoded = ctx.encode_and_encrypt( + b"Hello world!", + key=enc_key, + recipients=[r], + ) + # The recipient side: + assert b"Hello world!" == ctx.decode(encoded, shared_key, context={"alg": "A256GCM"}) + +AES Key Wrap +------------ + +The AES key wrap algorithm can be used to wrap an encryption key as follows: + +.. code-block:: python + + from cwt import COSE, COSEKey, Recipient + + # The sender side: + r = Recipient.from_jwk( + { + "kty": "oct", + "alg": "A128KW", + "kid": "01", + "k": "hJtXIZ2uSN5kbQfbtTNWbg", # A shared wrapping key + }, + ) + enc_key = COSEKey.from_symmetric_key(alg="ChaCha20/Poly1305") + r.apply(enc_key) + ctx = COSE.new(alg_auto_inclusion=True) + encoded = ctx.encode_and_encrypt(b"Hello world!", key=enc_key, recipients=[r]) + + # The recipient side: + shared_key = COSEKey.from_jwk( + { + "kty": "oct", + "alg": "A128KW", + "kid": "01", + "k": "hJtXIZ2uSN5kbQfbtTNWbg", + }, + ) + assert b"Hello world!" == ctx.decode(encoded, shared_key) + Direct Key Agreement -------------------- The direct key agreement methods can be used to create a shared secret. A KDF (Key Distribution Function) is then -applied the shared secret to derive a key to be used to protect the data. +applied to the shared secret to derive a key to be used to protect the data. The follwing example shows a simple way to make a COSE Encrypt message, verify and decode it with the direct key agreement methods (``ECDH-ES+HKDF-256`` with various curves). @@ -241,7 +550,7 @@ agreement methods (``ECDH-ES+HKDF-256`` with various curves). from cwt import COSE, COSEKey, Recipient # The sender side: - recipient = Recipient.from_jwk( + r = Recipient.from_jwk( { "kty": "EC", "alg": "ECDH-ES+HKDF-256", @@ -259,12 +568,12 @@ agreement methods (``ECDH-ES+HKDF-256`` with various curves). "y": "HlLtdXARY_f55A3fnzQbPcm6hgr34Mp8p-nuzQCE0Zw", } ) - enc_key = recipient.derive_key({"alg": "A128GCM"}, public_key=pub_key) + enc_key = r.apply(recipient_key=pub_key, context={"alg": "A128GCM"}) ctx = COSE.new(alg_auto_inclusion=True) encoded = ctx.encode_and_encrypt( b"Hello world!", key=enc_key, - recipients=[recipient], + recipients=[r], ) # The recipient side: @@ -294,7 +603,7 @@ In case of ``X25519``: from cwt import COSE, COSEKey, Recipient # The sender side: - recipient = Recipient.from_jwk( + r = Recipient.from_jwk( { "kty": "OKP", "alg": "ECDH-ES+HKDF-256", @@ -310,12 +619,12 @@ In case of ``X25519``: "x": "y3wJq3uXPHeoCO4FubvTc7VcBuqpvUrSvU6ZMbHDTCI", } ) - enc_key = recipient.derive_key({"alg": "A128GCM"}, public_key=pub_key) + enc_key = r.apply(recipient_key=pub_key, context={"alg": "A128GCM"}) ctx = COSE.new(alg_auto_inclusion=True) encoded = ctx.encode_and_encrypt( b"Hello world!", key=enc_key, - recipients=[recipient], + recipients=[r], ) # The recipient side: @@ -337,7 +646,7 @@ In case of ``X448``: from cwt import COSE, COSEKey, Recipient - recipient = Recipient.from_jwk( + r = Recipient.from_jwk( { "kty": "OKP", "alg": "ECDH-ES+HKDF-256", @@ -353,12 +662,12 @@ In case of ``X448``: "x": "IkLmc0klvEMXYneHMKAB6ePohryAwAPVe2pRSffIDY6NrjeYNWVX5J-fG4NV2OoU77C88A0mvxI", } ) - enc_key = recipient.derive_key({"alg": "A128GCM"}, public_key=pub_key) + enc_key = r.apply(recipient_key=pub_key, context={"alg": "A128GCM"}) ctx = COSE.new(alg_auto_inclusion=True) encoded = ctx.encode_and_encrypt( b"Hello world!", key=enc_key, - recipients=[recipient], + recipients=[r], ) priv_key = COSEKey.from_jwk( { @@ -373,6 +682,59 @@ In case of ``X448``: assert b"Hello world!" == ctx.decode(encoded, priv_key, context={"alg": "A128GCM"}) +Key Agreement with Key Wrap +--------------------------- + +.. code-block:: python + + from cwt import COSE, COSEKey, Recipient + + # The sender side: + enc_key = COSEKey.from_symmetric_key(alg="A128GCM") + nonce = enc_key.generate_nonce() + r = Recipient.from_jwk( + { + "kty": "EC", + "crv": "P-256", + "alg": "ECDH-SS+A128KW", + "x": "7cvYCcdU22WCwW1tZXR8iuzJLWGcd46xfxO1XJs-SPU", + "y": "DzhJXgz9RI6TseNmwEfLoNVns8UmvONsPzQDop2dKoo", + "d": "Uqr4fay_qYQykwcNCB2efj_NFaQRRQ-6fHZm763jt5w", + } + ) + pub_key = COSEKey.from_jwk( + { + "kty": "EC", + "crv": "P-256", + "kid": "meriadoc.brandybuck@buckland.example", + "x": "Ze2loSV3wrroKUN_4zhwGhCqo3Xhu1td4QjeQ5wIVR0", + "y": "HlLtdXARY_f55A3fnzQbPcm6hgr34Mp8p-nuzQCE0Zw", + } + ) + r.apply(enc_key, recipient_key=pub_key, context={"alg": "A128GCM"}) + ctx = COSE.new(alg_auto_inclusion=True) + encoded = ctx.encode_and_encrypt( + b"Hello world!", + key=enc_key, + nonce=nonce, + recipients=[r], + ) + + # The recipient side: + priv_key = COSEKey.from_jwk( + { + "kty": "EC", + "crv": "P-256", + "alg": "ECDH-SS+A128KW", + "kid": "meriadoc.brandybuck@buckland.example", + "x": "Ze2loSV3wrroKUN_4zhwGhCqo3Xhu1td4QjeQ5wIVR0", + "y": "HlLtdXARY_f55A3fnzQbPcm6hgr34Mp8p-nuzQCE0Zw", + "d": "r_kHyZ-a06rmxM3yESK84r1otSg-aQcVStkRhA-iCM8", + } + ) + assert b"Hello world!" == ctx.decode(encoded, priv_key, context={"alg": "A128GCM"}) + + COSE Signature1 =============== diff --git a/tests/test_cose_sample.py b/tests/test_cose_sample.py index 5edfee6..2f9a8a6 100644 --- a/tests/test_cose_sample.py +++ b/tests/test_cose_sample.py @@ -58,21 +58,22 @@ def test_cose_usage_examples_cose_mac_direct(self): def test_cose_usage_examples_cose_mac_direct_hkdf_sha_256(self): - recipient = Recipient.from_jwk( + shared_material = token_bytes(32) + shared_key = COSEKey.from_symmetric_key(shared_material, kid="01") + + r = Recipient.from_jwk( { "kty": "oct", "alg": "direct+HKDF-SHA-256", "salt": "aabbccddeeffgghh", }, ) - shared_material = token_bytes(32) - shared_key = COSEKey.from_symmetric_key(shared_material, kid="01") - mac_key = recipient.apply(shared_key, context={"alg": "HS256"}) + mac_key = r.apply(shared_key, context={"alg": "HS256"}) ctx = COSE.new(alg_auto_inclusion=True) encoded = ctx.encode_and_mac( b"Hello world!", key=mac_key, - recipients=[recipient], + recipients=[r], ) assert b"Hello world!" == ctx.decode( encoded, shared_key, context={"alg": "HS256"} @@ -105,10 +106,175 @@ def test_cose_usage_examples_cose_mac_aes_key_wrap(self): ) assert b"Hello world!" == ctx.decode(encoded, shared_key) + def test_cose_usage_examples_cose_mac_ecdh_direct_hkdf_p256(self): + + r = Recipient.from_jwk( + { + "kty": "EC", + "alg": "ECDH-ES+HKDF-256", + "crv": "P-256", + }, + ) + pub_key = COSEKey.from_jwk( + { + "kty": "EC", + "kid": "01", + "crv": "P-256", + "x": "Ze2loSV3wrroKUN_4zhwGhCqo3Xhu1td4QjeQ5wIVR0", + "y": "HlLtdXARY_f55A3fnzQbPcm6hgr34Mp8p-nuzQCE0Zw", + } + ) + mac_key = r.apply(recipient_key=pub_key, context={"alg": "HS256"}) + ctx = COSE.new(alg_auto_inclusion=True) + encoded = ctx.encode_and_mac( + b"Hello world!", + key=mac_key, + recipients=[r], + ) + priv_key = COSEKey.from_jwk( + { + "kty": "EC", + "alg": "ECDH-ES+HKDF-256", + "kid": "01", + "crv": "P-256", + "x": "Ze2loSV3wrroKUN_4zhwGhCqo3Xhu1td4QjeQ5wIVR0", + "y": "HlLtdXARY_f55A3fnzQbPcm6hgr34Mp8p-nuzQCE0Zw", + "d": "r_kHyZ-a06rmxM3yESK84r1otSg-aQcVStkRhA-iCM8", + } + ) + assert b"Hello world!" == ctx.decode( + encoded, priv_key, context={"alg": "HS256"} + ) + + def test_cose_usage_examples_cose_mac_ecdh_direct_hkdf_x25519(self): + + r = Recipient.from_jwk( + { + "kty": "OKP", + "alg": "ECDH-ES+HKDF-256", + "crv": "X25519", + }, + ) + pub_key = COSEKey.from_jwk( + { + "kty": "OKP", + "alg": "ECDH-ES+HKDF-256", + "kid": "01", + "crv": "X25519", + "x": "y3wJq3uXPHeoCO4FubvTc7VcBuqpvUrSvU6ZMbHDTCI", + } + ) + mac_key = r.apply(recipient_key=pub_key, context={"alg": "HS256"}) + ctx = COSE.new(alg_auto_inclusion=True) + encoded = ctx.encode_and_mac( + b"Hello world!", + key=mac_key, + recipients=[r], + ) + priv_key = COSEKey.from_jwk( + { + "kty": "OKP", + "alg": "ECDH-ES+HKDF-256", + "kid": "01", + "crv": "X25519", + "x": "y3wJq3uXPHeoCO4FubvTc7VcBuqpvUrSvU6ZMbHDTCI", + "d": "vsJ1oX5NNi0IGdwGldiac75r-Utmq3Jq4LGv48Q_Qc4", + } + ) + assert b"Hello world!" == ctx.decode( + encoded, priv_key, context={"alg": "HS256"} + ) + + def test_cose_usage_examples_cose_mac_ecdh_direct_hkdf_x448(self): + + r = Recipient.from_jwk( + { + "kty": "OKP", + "alg": "ECDH-ES+HKDF-256", + "crv": "X448", + }, + ) + pub_key = COSEKey.from_jwk( + { + "kty": "OKP", + "alg": "ECDH-ES+HKDF-256", + "kid": "01", + "crv": "X448", + "x": "IkLmc0klvEMXYneHMKAB6ePohryAwAPVe2pRSffIDY6NrjeYNWVX5J-fG4NV2OoU77C88A0mvxI", + } + ) + mac_key = r.apply(recipient_key=pub_key, context={"alg": "HS256"}) + ctx = COSE.new(alg_auto_inclusion=True) + encoded = ctx.encode_and_mac( + b"Hello world!", + key=mac_key, + recipients=[r], + ) + priv_key = COSEKey.from_jwk( + { + "kty": "OKP", + "alg": "ECDH-ES+HKDF-256", + "kid": "01", + "crv": "X448", + "x": "IkLmc0klvEMXYneHMKAB6ePohryAwAPVe2pRSffIDY6NrjeYNWVX5J-fG4NV2OoU77C88A0mvxI", + "d": "rJJRG3nshyCtd9CgXld8aNaB9YXKR0UOi7zj7hApg9YH4XdBO0G8NcAFNz_uPH2GnCZVcSDgV5c", + } + ) + assert b"Hello world!" == ctx.decode( + encoded, priv_key, context={"alg": "HS256"} + ) + + def test_cose_usage_examples_cose_mac_ecdh_ss_a128kw(self): + + # The sender side: + mac_key = COSEKey.from_symmetric_key(alg="HS256") + r = Recipient.from_jwk( + { + "kty": "EC", + "crv": "P-256", + "alg": "ECDH-SS+A128KW", + "x": "7cvYCcdU22WCwW1tZXR8iuzJLWGcd46xfxO1XJs-SPU", + "y": "DzhJXgz9RI6TseNmwEfLoNVns8UmvONsPzQDop2dKoo", + "d": "Uqr4fay_qYQykwcNCB2efj_NFaQRRQ-6fHZm763jt5w", + } + ) + pub_key = COSEKey.from_jwk( + { + "kty": "EC", + "crv": "P-256", + "kid": "meriadoc.brandybuck@buckland.example", + "x": "Ze2loSV3wrroKUN_4zhwGhCqo3Xhu1td4QjeQ5wIVR0", + "y": "HlLtdXARY_f55A3fnzQbPcm6hgr34Mp8p-nuzQCE0Zw", + } + ) + r.apply(mac_key, recipient_key=pub_key, context={"alg": "HS256"}) + ctx = COSE.new(alg_auto_inclusion=True) + encoded = ctx.encode_and_mac( + b"Hello world!", + key=mac_key, + recipients=[r], + ) + + # The recipient side: + priv_key = COSEKey.from_jwk( + { + "kty": "EC", + "crv": "P-256", + "alg": "ECDH-SS+A128KW", + "kid": "meriadoc.brandybuck@buckland.example", + "x": "Ze2loSV3wrroKUN_4zhwGhCqo3Xhu1td4QjeQ5wIVR0", + "y": "HlLtdXARY_f55A3fnzQbPcm6hgr34Mp8p-nuzQCE0Zw", + "d": "r_kHyZ-a06rmxM3yESK84r1otSg-aQcVStkRhA-iCM8", + } + ) + assert b"Hello world!" == ctx.decode( + encoded, priv_key, context={"alg": "HS256"} + ) + def test_cose_usage_examples_cose_encrypt0(self): enc_key = COSEKey.from_symmetric_key(alg="ChaCha20/Poly1305", kid="01") - nonce = enc_key.generate_nonce() + nonce = enc_key.generate_nonce() ctx = COSE.new(alg_auto_inclusion=True, kid_auto_inclusion=True) encoded = ctx.encode_and_encrypt(b"Hello world!", enc_key, nonce=nonce) assert b"Hello world!" == ctx.decode(encoded, enc_key) @@ -137,25 +303,25 @@ def test_cose_usage_examples_cose_encrypt0(self): def test_cose_usage_examples_cose_encrypt(self): enc_key = COSEKey.from_symmetric_key(alg="ChaCha20/Poly1305", kid="01") nonce = enc_key.generate_nonce() - recipient = Recipient.from_jwk({"alg": "direct"}) - recipient.apply(enc_key) + r = Recipient.from_jwk({"alg": "direct"}) + r.apply(enc_key) ctx = COSE.new() encoded = ctx.encode_and_encrypt( b"Hello world!", enc_key, nonce=nonce, - recipients=[recipient], + recipients=[r], ) assert b"Hello world!" == ctx.decode(encoded, enc_key) - recipient = Recipient.new(unprotected={"alg": "direct"}) - recipient.apply(enc_key) + r = Recipient.new(unprotected={"alg": "direct"}) + r.apply(enc_key) encoded2 = ctx.encode_and_encrypt( b"Hello world!", enc_key, nonce=nonce, - recipients=[recipient], + recipients=[r], ) assert b"Hello world!" == ctx.decode(encoded2, enc_key) @@ -163,7 +329,7 @@ def test_cose_usage_examples_cose_encrypt(self): b"Hello world!", enc_key, nonce=nonce, - recipients=[recipient], + recipients=[r], ) assert b"Hello world!" == ctx.decode(encoded3, enc_key) @@ -171,21 +337,22 @@ def test_cose_usage_examples_cose_encrypt(self): def test_cose_usage_examples_cose_encrypt_direct_hkdf_sha_256(self): - recipient = Recipient.from_jwk( + shared_material = token_bytes(32) + shared_key = COSEKey.from_symmetric_key(shared_material, kid="01") + + r = Recipient.from_jwk( { "kty": "oct", "alg": "direct+HKDF-SHA-256", "salt": "aabbccddeeffgghh", }, ) - shared_material = token_bytes(32) - shared_key = COSEKey.from_symmetric_key(shared_material, kid="01") - enc_key = recipient.apply(shared_key, context={"alg": "A256GCM"}) + enc_key = r.apply(shared_key, context={"alg": "A256GCM"}) ctx = COSE.new(alg_auto_inclusion=True) encoded = ctx.encode_and_encrypt( b"Hello world!", key=enc_key, - recipients=[recipient], + recipients=[r], ) assert b"Hello world!" == ctx.decode( encoded, shared_key, context={"alg": "A256GCM"} @@ -218,7 +385,7 @@ def test_cose_usage_examples_cose_encrypt_aes_key_wrap_a128kw(self): def test_cose_usage_examples_cose_encrypt_ecdh_direct_hkdf_p256(self): - recipient = Recipient.from_jwk( + r = Recipient.from_jwk( { "kty": "EC", "alg": "ECDH-ES+HKDF-256", @@ -234,12 +401,12 @@ def test_cose_usage_examples_cose_encrypt_ecdh_direct_hkdf_p256(self): "y": "HlLtdXARY_f55A3fnzQbPcm6hgr34Mp8p-nuzQCE0Zw", } ) - enc_key = recipient.apply(recipient_key=pub_key, context={"alg": "A128GCM"}) + enc_key = r.apply(recipient_key=pub_key, context={"alg": "A128GCM"}) ctx = COSE.new(alg_auto_inclusion=True) encoded = ctx.encode_and_encrypt( b"Hello world!", key=enc_key, - recipients=[recipient], + recipients=[r], ) priv_key = COSEKey.from_jwk( { @@ -258,7 +425,7 @@ def test_cose_usage_examples_cose_encrypt_ecdh_direct_hkdf_p256(self): def test_cose_usage_examples_cose_encrypt_ecdh_direct_hkdf_x25519(self): - recipient = Recipient.from_jwk( + r = Recipient.from_jwk( { "kty": "OKP", "alg": "ECDH-ES+HKDF-256", @@ -274,12 +441,12 @@ def test_cose_usage_examples_cose_encrypt_ecdh_direct_hkdf_x25519(self): "x": "y3wJq3uXPHeoCO4FubvTc7VcBuqpvUrSvU6ZMbHDTCI", } ) - enc_key = recipient.apply(recipient_key=pub_key, context={"alg": "A128GCM"}) + enc_key = r.apply(recipient_key=pub_key, context={"alg": "A128GCM"}) ctx = COSE.new(alg_auto_inclusion=True) encoded = ctx.encode_and_encrypt( b"Hello world!", key=enc_key, - recipients=[recipient], + recipients=[r], ) priv_key = COSEKey.from_jwk( { @@ -297,7 +464,7 @@ def test_cose_usage_examples_cose_encrypt_ecdh_direct_hkdf_x25519(self): def test_cose_usage_examples_cose_encrypt_ecdh_direct_hkdf_x448(self): - recipient = Recipient.from_jwk( + r = Recipient.from_jwk( { "kty": "OKP", "alg": "ECDH-ES+HKDF-256", @@ -313,12 +480,12 @@ def test_cose_usage_examples_cose_encrypt_ecdh_direct_hkdf_x448(self): "x": "IkLmc0klvEMXYneHMKAB6ePohryAwAPVe2pRSffIDY6NrjeYNWVX5J-fG4NV2OoU77C88A0mvxI", } ) - enc_key = recipient.apply(recipient_key=pub_key, context={"alg": "A128GCM"}) + enc_key = r.apply(recipient_key=pub_key, context={"alg": "A128GCM"}) ctx = COSE.new(alg_auto_inclusion=True) encoded = ctx.encode_and_encrypt( b"Hello world!", key=enc_key, - recipients=[recipient], + recipients=[r], ) priv_key = COSEKey.from_jwk( { @@ -334,6 +501,55 @@ def test_cose_usage_examples_cose_encrypt_ecdh_direct_hkdf_x448(self): encoded, priv_key, context={"alg": "A128GCM"} ) + def test_cose_usage_examples_cose_encrypt_ecdh_ss_a128kw(self): + + # The sender side: + enc_key = COSEKey.from_symmetric_key(alg="A128GCM") + nonce = enc_key.generate_nonce() + r = Recipient.from_jwk( + { + "kty": "EC", + "crv": "P-256", + "alg": "ECDH-SS+A128KW", + "x": "7cvYCcdU22WCwW1tZXR8iuzJLWGcd46xfxO1XJs-SPU", + "y": "DzhJXgz9RI6TseNmwEfLoNVns8UmvONsPzQDop2dKoo", + "d": "Uqr4fay_qYQykwcNCB2efj_NFaQRRQ-6fHZm763jt5w", + } + ) + pub_key = COSEKey.from_jwk( + { + "kty": "EC", + "crv": "P-256", + "kid": "meriadoc.brandybuck@buckland.example", + "x": "Ze2loSV3wrroKUN_4zhwGhCqo3Xhu1td4QjeQ5wIVR0", + "y": "HlLtdXARY_f55A3fnzQbPcm6hgr34Mp8p-nuzQCE0Zw", + } + ) + r.apply(enc_key, recipient_key=pub_key, context={"alg": "A128GCM"}) + ctx = COSE.new(alg_auto_inclusion=True) + encoded = ctx.encode_and_encrypt( + b"Hello world!", + key=enc_key, + nonce=nonce, + recipients=[r], + ) + + # The recipient side: + priv_key = COSEKey.from_jwk( + { + "kty": "EC", + "crv": "P-256", + "alg": "ECDH-SS+A128KW", + "kid": "meriadoc.brandybuck@buckland.example", + "x": "Ze2loSV3wrroKUN_4zhwGhCqo3Xhu1td4QjeQ5wIVR0", + "y": "HlLtdXARY_f55A3fnzQbPcm6hgr34Mp8p-nuzQCE0Zw", + "d": "r_kHyZ-a06rmxM3yESK84r1otSg-aQcVStkRhA-iCM8", + } + ) + assert b"Hello world!" == ctx.decode( + encoded, priv_key, context={"alg": "A128GCM"} + ) + def test_cose_usage_examples_cose_signature1(self): priv_key = COSEKey.from_jwk( @@ -380,7 +596,7 @@ def test_cose_usage_examples_cose_signature1(self): def test_cose_usage_examples_cose_encrypt_ecdh_aes_key_wrap(self): enc_key = COSEKey.from_symmetric_key(alg="A128GCM") - recipient = Recipient.from_jwk( + r = Recipient.from_jwk( { "kty": "EC", "alg": "ECDH-ES+A128KW", @@ -397,12 +613,12 @@ def test_cose_usage_examples_cose_encrypt_ecdh_aes_key_wrap(self): "y": "HlLtdXARY_f55A3fnzQbPcm6hgr34Mp8p-nuzQCE0Zw", } ) - recipient.apply(enc_key, recipient_key=pub_key, context={"alg": "A128GCM"}) + r.apply(enc_key, recipient_key=pub_key, context={"alg": "A128GCM"}) ctx = COSE.new(alg_auto_inclusion=True) encoded = ctx.encode_and_encrypt( b"Hello world!", key=enc_key, - recipients=[recipient], + recipients=[r], ) priv_key = COSEKey.from_jwk( {