def _validate(self): """ Reference: https://docs.docker.com/registry/spec/manifest-v2-1/#signed-manifests """ if not self._signatures: return payload_str = self._payload for signature in self._signatures: protected = signature[DOCKER_SCHEMA1_PROTECTED_KEY] sig = signature[DOCKER_SCHEMA1_SIGNATURE_KEY] jwk = JsonWebKey.import_key( signature[DOCKER_SCHEMA1_HEADER_KEY]["jwk"]) jws = JsonWebSignature( algorithms=[signature[DOCKER_SCHEMA1_HEADER_KEY]["alg"]]) obj_to_verify = { DOCKER_SCHEMA1_PROTECTED_KEY: protected, DOCKER_SCHEMA1_SIGNATURE_KEY: sig, DOCKER_SCHEMA1_HEADER_KEY: { "alg": signature[DOCKER_SCHEMA1_HEADER_KEY]["alg"] }, "payload": base64url_encode(payload_str), } try: data = jws.deserialize_json(obj_to_verify, jwk.get_public_key()) except (BadSignatureError, UnsupportedAlgorithmError): raise InvalidSchema1Signature() if not data: raise InvalidSchema1Signature()
def test_function_key(self): protected = {'alg': 'HS256'} header = [ { 'protected': protected, 'header': { 'kid': 'a' } }, { 'protected': protected, 'header': { 'kid': 'b' } }, ] def load_key(header, payload): self.assertEqual(payload, b'hello') kid = header.get('kid') if kid == 'a': return 'secret-a' return 'secret-b' jws = JsonWebSignature(algorithms=JWS_ALGORITHMS) s = jws.serialize(header, b'hello', load_key) self.assertIsInstance(s, dict) self.assertIn('signatures', s) data = jws.deserialize(json.dumps(s), load_key) header, payload = data['header'], data['payload'] self.assertEqual(payload, b'hello') self.assertEqual(header[0]['alg'], 'HS256') self.assertNotIn('signature', data)
def test_compact_jws(self): jws = JsonWebSignature(algorithms=JWS_ALGORITHMS) s = jws.serialize({'alg': 'HS256'}, 'hello', 'secret') data = jws.deserialize(s, 'secret') header, payload = data['header'], data['payload'] self.assertEqual(payload, b'hello') self.assertEqual(header['alg'], 'HS256') self.assertNotIn('signature', data)
def get_jwt_token(self, user): now = int(time()) rand = ''.join([choice(string.ascii_letters) for _ in range(10)]) rands = f'{rand}.{now}' jws = JsonWebSignature(algorithms=JWS_ALGORITHMS) headers = {'alg': 'HS256'} payload = json.dumps({'email': user.email}) secret = bytes(self.app.config['SECRET_KEY'], 'utf-8') return jws.serialize_compact(headers, payload, secret)
def test_ES256K_alg(self): jws = JsonWebSignature(algorithms=['ES256K']) private_key = read_file_path('secp256k1-private.pem') public_key = read_file_path('secp256k1-pub.pem') s = jws.serialize({'alg': 'ES256K'}, 'hello', private_key) data = jws.deserialize(s, public_key) header, payload = data['header'], data['payload'] self.assertEqual(payload, b'hello') self.assertEqual(header['alg'], 'ES256K')
def test_EdDSA_alg(self): jws = JsonWebSignature(algorithms=RFC8037_ALGORITHMS) private_key = read_file_path('ed25519-pkcs8.pem') public_key = read_file_path('ed25519-pub.pem') s = jws.serialize({'alg': 'EdDSA'}, 'hello', private_key) data = jws.deserialize(s, public_key) header, payload = data['header'], data['payload'] self.assertEqual(payload, b'hello') self.assertEqual(header['alg'], 'EdDSA')
def test_ES512_alg(self): jws = JsonWebSignature() private_key = read_file_path('secp521r1-private.json') public_key = read_file_path('secp521r1-public.json') self.assertRaises(ValueError, jws.serialize, {'alg': 'ES256'}, 'hello', private_key) s = jws.serialize({'alg': 'ES512'}, 'hello', private_key) data = jws.deserialize(s, public_key) header, payload = data['header'], data['payload'] self.assertEqual(payload, b'hello') self.assertEqual(header['alg'], 'ES512')
def test_validate_header(self): jws = JsonWebSignature(algorithms=JWS_ALGORITHMS) protected = {'alg': 'HS256', 'invalid': 'k'} header = {'protected': protected, 'header': {'kid': 'a'}} self.assertRaises(errors.InvalidHeaderParameterName, jws.serialize, header, b'hello', 'secret') jws = JsonWebSignature(algorithms=JWS_ALGORITHMS, private_headers=['invalid']) s = jws.serialize(header, b'hello', 'secret') self.assertIsInstance(s, dict)
def test_compact_rsa_pss(self): jws = JsonWebSignature(algorithms=JWS_ALGORITHMS) private_key = read_file_path('rsa_private.pem') public_key = read_file_path('rsa_public.pem') s = jws.serialize({'alg': 'PS256'}, 'hello', private_key) data = jws.deserialize(s, public_key) header, payload = data['header'], data['payload'] self.assertEqual(payload, b'hello') self.assertEqual(header['alg'], 'PS256') ssh_pub_key = read_file_path('ssh_public.pem') self.assertRaises(errors.BadSignatureError, jws.deserialize, s, ssh_pub_key)
def test_flattened_json_jws(self): jws = JsonWebSignature(algorithms=JWS_ALGORITHMS) protected = {'alg': 'HS256'} header = {'protected': protected, 'header': {'kid': 'a'}} s = jws.serialize(header, 'hello', 'secret') self.assertIsInstance(s, dict) data = jws.deserialize(s, 'secret') header, payload = data['header'], data['payload'] self.assertEqual(payload, b'hello') self.assertEqual(header['alg'], 'HS256') self.assertNotIn('protected', data)
def test_compact_rsa(self): jws = JsonWebSignature() private_key = read_file_path('rsa_private.pem') public_key = read_file_path('rsa_public.pem') s = jws.serialize({'alg': 'RS256'}, 'hello', private_key) data = jws.deserialize(s, public_key) header, payload = data['header'], data['payload'] self.assertEqual(payload, b'hello') self.assertEqual(header['alg'], 'RS256') # can deserialize with private key data2 = jws.deserialize(s, private_key) self.assertEqual(data, data2) ssh_pub_key = read_file_path('ssh_public.pem') self.assertRaises(errors.BadSignatureError, jws.deserialize, s, ssh_pub_key)
def test_nested_json_jws(self): jws = JsonWebSignature(algorithms=JWS_ALGORITHMS) protected = {'alg': 'HS256'} header = {'protected': protected, 'header': {'kid': 'a'}} s = jws.serialize([header], 'hello', 'secret') self.assertIsInstance(s, dict) self.assertIn('signatures', s) data = jws.deserialize(s, 'secret') header, payload = data['header'], data['payload'] self.assertEqual(payload, b'hello') self.assertEqual(header[0]['alg'], 'HS256') self.assertNotIn('signatures', data) # test bad signature self.assertRaises(errors.BadSignatureError, jws.deserialize, s, 'f')
def test_invalid_alg(self): jws = JsonWebSignature(algorithms=JWS_ALGORITHMS) self.assertRaises(errors.UnsupportedAlgorithmError, jws.deserialize, 'eyJhbGciOiJzIn0.YQ.YQ', 'k') self.assertRaises(errors.MissingAlgorithmError, jws.serialize, {}, '', 'k') self.assertRaises(errors.UnsupportedAlgorithmError, jws.serialize, {'alg': 's'}, '', 'k')
def verify_credential(signed_credential, did): """ Verify credential signed with RSA key of the DID @parma signed_credential as a dict @param did as a str return bool """ read = requests.get('https://talao.co/resolver?did=' + did) for Key in read.json()['publicKey']: if Key.get('id') == did + "#secondary": public_key = Key['publicKeyPem'] break jws = JsonWebSignature() try: jws.deserialize_compact(signed_credential['proof']['jws'], public_key) except: return False return True
def json_verify(msg: Union[bytes, dict, str], pubKey: Union[str, Dict[str, str]] = None) -> bool: msg: dict = msg if isinstance(msg, dict) else json.loads(msg) if signature := msg.pop("signature", None): sig = signature.split(".") header = json.loads(base64.b64decode(sig[0])) sig[1] = base64.b64encode(b'.'.join([ base64.b64encode(canonicaljson.encode_canonical_json(header)), base64.b64encode(canonicaljson.encode_canonical_json(msg)) ])).decode("utf-8").rstrip("=") jws = JsonWebSignature() key = Path(pubKey).read_bytes() if isinstance( pubKey, str) else partial(load_key, keys=pubKey) try: jws.deserialize_compact(".".join(sig), key) return True except errors.BadSignatureError: return False
def json_sign(msg: Union[dict, str], privKey: str) -> dict: msg: dict = json.loads(msg) if isinstance(msg, str) else msg if not privKey: raise ValueError("privKey was not passed as a param") header = { "alg": "ES256", "kid": msg.get("headers", {}).get("from", "ORIGIN") } sig_payload = b".".join([ base64.b64encode(canonicaljson.encode_canonical_json(header)), base64.b64encode(canonicaljson.encode_canonical_json(msg)) ]) jws = JsonWebSignature() key = Path(privKey).read_bytes() sig = jws.serialize_compact(header, sig_payload, key).decode("utf-8").split('.') msg["signature"] = f"{sig[0]}..{sig[2]}" return msg
def sign_credential(credential, key): """ Sign credential with RSA key of the did, add the signature as linked data JSONLD @parma credential as a dict #param key a string PEM private RSA key return signed credential as a dict """ payload = json.dumps(credential) credential_jws = JsonWebSignature(algorithms=['RS256']) protected = {'alg': 'RS256'} signature = credential_jws.serialize_compact(protected, payload, key.encode()).decode() credential["proof"] = { "type": "RsaSignature2018", "created": datetime.now().strftime("%m/%d/%Y, %H:%M:%S"), "proofPurpose": "assertionMethod", "verificationMethod": "https://talao.readthedocs.io/en/latest/", "jws": signature } return credential
def test_register_invalid_algorithms(self): self.assertRaises( ValueError, JsonWebSignature, ['INVALID'] ) jws = JsonWebSignature(algorithms=[]) self.assertRaises( ValueError, jws.register_algorithm, JWE_ALGORITHMS[0] )
def test_fail_deserialize_json(self): jws = JsonWebSignature(algorithms=JWS_ALGORITHMS) self.assertRaises(errors.DecodeError, jws.deserialize_json, None, '') self.assertRaises(errors.DecodeError, jws.deserialize_json, '[]', '') self.assertRaises(errors.DecodeError, jws.deserialize_json, '{}', '') # missing protected s = json.dumps({'payload': 'YQ'}) self.assertRaises(errors.DecodeError, jws.deserialize_json, s, '') # missing signature s = json.dumps({'payload': 'YQ', 'protected': 'YQ'}) self.assertRaises(errors.DecodeError, jws.deserialize_json, s, '')
def test_invalid_input(self): jws = JsonWebSignature(algorithms=JWS_ALGORITHMS) self.assertRaises(errors.DecodeError, jws.deserialize, 'a', 'k') self.assertRaises(errors.DecodeError, jws.deserialize, 'a.b.c', 'k') self.assertRaises(errors.DecodeError, jws.deserialize, 'YQ.YQ.YQ', 'k') # a self.assertRaises(errors.DecodeError, jws.deserialize, 'W10.a.YQ', 'k') # [] self.assertRaises(errors.DecodeError, jws.deserialize, 'e30.a.YQ', 'k') # {} self.assertRaises(errors.DecodeError, jws.deserialize, 'eyJhbGciOiJzIn0.a.YQ', 'k') self.assertRaises(errors.DecodeError, jws.deserialize, 'eyJhbGciOiJzIn0.YQ.a', 'k')
def get_token(self): """Return a JWT token and expiration time for making API calls to Small World Community Arguments to this function will, by default, use environmental variables if they are present (noted below) :param community_domain: the FQDN of the community instance (SWC_AUDIENCE) :param app_id: the the Oauth Application ID being used to connect to the community API (SWC_APP_ID) :param user_id: The SWC User ID of the authorized user attached to this Oauth Application (SWC_USER_ID) :return: A tuple with token to use with request calls and the time (in seconds) when the token will expire """ exp = int(time.time()) + TOKEN_DURATION payload = { "iss": self.app_id, "iat": int(time.time()), "aud": self.community_domain, "exp": exp, "sub": self.user_id, "scope": SWC_SCOPE, } jws = JsonWebSignature(["HS256"]) auth_token = jws.serialize_compact( JWT_TOKEN_HEADER, bytearray(json.dumps(payload), "utf-8"), bytearray(self.app_secret, "utf-8"), ) swc_token_url = f"https://{self.community_domain}/services/4.0/token" token_request = requests.post( swc_token_url, data={ "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer", "assertion": auth_token.decode("utf-8"), "content_type": "application/x-www-form-urlencoded", }, ) token_request.raise_for_status() token = token_request.json().get("access_token") return token, exp
def test_not_supported_alg(self): jws = JsonWebSignature(algorithms=['HS256']) s = jws.serialize({'alg': 'HS256'}, 'hello', 'secret') jws = JsonWebSignature(algorithms=['RS256']) self.assertRaises( errors.UnsupportedAlgorithmError, lambda: jws.serialize({'alg': 'HS256'}, 'hello', 'secret')) self.assertRaises(errors.UnsupportedAlgorithmError, jws.deserialize, s, 'secret')
def build(self, json_web_key=None, ensure_ascii=True): """ Builds a DockerSchema1Manifest object, with optional signature. NOTE: For backward compatibility, "JWS JSON Serialization" is used instead of "JWS Compact Serialization", since the latter **requires** that the "alg" headers be carried in the **protected** headers, which was never done before migrating to authlib (One shouldn't be using schema1 anyways) References: - https://tools.ietf.org/html/rfc7515#section-10.7 - https://docs.docker.com/registry/spec/manifest-v2-1/#signed-manifests """ payload = OrderedDict(self._base_payload) payload.update({ DOCKER_SCHEMA1_HISTORY_KEY: self._history, DOCKER_SCHEMA1_FS_LAYERS_KEY: self._fs_layer_digests, }) payload_str = json.dumps(payload, indent=3, ensure_ascii=ensure_ascii) if json_web_key is None: return DockerSchema1Manifest( Bytes.for_string_or_unicode(payload_str)) payload_str = Bytes.for_string_or_unicode(payload_str).as_encoded_str() split_point = payload_str.rfind(b"\n}") protected_payload = { DOCKER_SCHEMA1_FORMAT_TAIL_KEY: base64url_encode(payload_str[split_point:]).decode("ascii"), DOCKER_SCHEMA1_FORMAT_LENGTH_KEY: split_point, "time": datetime.utcnow().strftime(_ISO_DATETIME_FORMAT_ZULU), } # Flattened JSON serialization header jws = JsonWebSignature(algorithms=[_JWS_SIGNING_ALGORITHM]) headers = { "protected": protected_payload, "header": { "alg": _JWS_SIGNING_ALGORITHM }, } signed = jws.serialize_json(headers, payload_str, json_web_key.get_private_key()) protected = signed["protected"] signature = signed["signature"] logger.debug("Generated signature: %s", signature) logger.debug("Generated protected block: %s", protected) public_members = set(json_web_key.REQUIRED_JSON_FIELDS + json_web_key.ALLOWED_PARAMS) public_key = { comp: value for comp, value in list(json_web_key.as_dict().items()) if comp in public_members } public_key["kty"] = json_web_key.kty signature_block = { DOCKER_SCHEMA1_HEADER_KEY: { "jwk": public_key, "alg": _JWS_SIGNING_ALGORITHM }, DOCKER_SCHEMA1_SIGNATURE_KEY: signature, DOCKER_SCHEMA1_PROTECTED_KEY: protected, } logger.debug("Encoded signature block: %s", json.dumps(signature_block)) payload.update({DOCKER_SCHEMA1_SIGNATURES_KEY: [signature_block]}) json_str = json.dumps(payload, indent=3, ensure_ascii=ensure_ascii) return DockerSchema1Manifest(Bytes.for_string_or_unicode(json_str))
def build(self, json_web_key=None, ensure_ascii=True): """ Builds a DockerSchema1Manifest object, with optional signature. NOTE: For backward compatibility, "JWS JSON Serialization" is used instead of "JWS Compact Serialization", since the latter **requires** that the "alg" headers be carried in the **protected** headers, which was never done before migrating to authlib (One shouldn't be using schema1 anyways) References: - https://tools.ietf.org/html/rfc7515#section-10.7 - https://docs.docker.com/registry/spec/manifest-v2-1/#signed-manifests """ payload = OrderedDict(self._base_payload) payload.update({ DOCKER_SCHEMA1_HISTORY_KEY: self._history, DOCKER_SCHEMA1_FS_LAYERS_KEY: self._fs_layer_digests, }) payload_str = json.dumps(payload, indent=3, ensure_ascii=ensure_ascii) if json_web_key is None: return DockerSchema1Manifest( Bytes.for_string_or_unicode(payload_str)) payload_str = Bytes.for_string_or_unicode(payload_str).as_encoded_str() split_point = payload_str.rfind(b"\n}") protected_payload = { DOCKER_SCHEMA1_FORMAT_TAIL_KEY: base64url_encode(payload_str[split_point:]).decode("ascii"), DOCKER_SCHEMA1_FORMAT_LENGTH_KEY: split_point, "time": datetime.utcnow().strftime(_ISO_DATETIME_FORMAT_ZULU), } # Flattened JSON serialization header jws = JsonWebSignature(algorithms=[_JWS_SIGNING_ALGORITHM]) headers = { "protected": protected_payload, "header": { "alg": _JWS_SIGNING_ALGORITHM }, } signed = jws.serialize_json(headers, payload_str, json_web_key.get_private_key()) protected = signed["protected"] signature = signed["signature"] logger.debug("Generated signature: %s", signature) logger.debug("Generated protected block: %s", protected) public_members = set(json_web_key.REQUIRED_JSON_FIELDS + json_web_key.ALLOWED_PARAMS) public_key = { comp: value for comp, value in list(json_web_key.as_dict().items()) if comp in public_members } public_key["kty"] = json_web_key.kty # Signed Docker schema 1 manifests require the kid to be in a specific format # https://docs.docker.com/registry/spec/auth/jwt/ pub_key = json_web_key.get_public_key() # Take the DER encoded public key which the JWT token was signed against key_der = pub_key.public_bytes( encoding=Encoding.DER, format=PublicFormat.SubjectPublicKeyInfo) # Create a SHA256 hash out of it and truncate to 240bits hash256 = sha256() hash256.update(key_der) digest = hash256.digest() digest_first_240_bits = digest[:30] # Split the result into 12 base32 encoded groups with : as delimiter base32 = base64.b32encode(digest_first_240_bits).decode("ascii") kid = "" i = 0 for i in range(0, int(len(base32) / 4) - 1): start = i * 4 end = start + 4 kid += base32[start:end] + ":" kid += base32[(i + 1) * 4:] # Add the last group without the delimiter public_key["kid"] = kid signature_block = { DOCKER_SCHEMA1_HEADER_KEY: { "jwk": public_key, "alg": _JWS_SIGNING_ALGORITHM }, DOCKER_SCHEMA1_SIGNATURE_KEY: signature, DOCKER_SCHEMA1_PROTECTED_KEY: protected, } logger.debug("Encoded signature block: %s", json.dumps(signature_block)) payload.update({DOCKER_SCHEMA1_SIGNATURES_KEY: [signature_block]}) json_str = json.dumps(payload, indent=3, ensure_ascii=ensure_ascii) return DockerSchema1Manifest(Bytes.for_string_or_unicode(json_str))
def validate_jwt_token(self, token): jws = JsonWebSignature(algorithms=JWS_ALGORITHMS) secret = bytes(self.app.config['SECRET_KEY'], 'utf-8') data = jws.deserialize_compact(token, secret) return json.loads(data['payload'])
def test_compact_none(self): jws = JsonWebSignature(algorithms=JWS_ALGORITHMS) s = jws.serialize({'alg': 'none'}, 'hello', '') self.assertRaises(errors.BadSignatureError, jws.deserialize, s, '')
def test_bad_signature(self): jws = JsonWebSignature(algorithms=JWS_ALGORITHMS) s = 'eyJhbGciOiJIUzI1NiJ9.YQ.YQ' self.assertRaises(errors.BadSignatureError, jws.deserialize, s, 'k')
def test_keys(): """Try to store/get/remove keys""" # JWS jws = JsonWebSignature(algorithms=["RS256"]) code_payload = { "user_id": "user", "scope": "scope", "client_id": "client", "redirect_uri": "redirect_uri", "code_challenge": "code_challenge", } # Token metadata header = {"alg": "RS256"} payload = { "sub": "user", "iss": "issuer", "scope": "scope", "setup": "setup", "group": "my_group" } # Remove all keys result = db.removeKeys() assert result["OK"], result["Message"] # Check active keys result = db.getActiveKeys() assert result["OK"], result["Message"] assert result["Value"] == [] # Create new one result = db.getPrivateKey() assert result["OK"], result["Message"] private_key = result["Value"] assert isinstance(private_key, RSAKey) # Sign token header["kid"] = private_key.thumbprint() # Find key by KID result = db.getPrivateKey(header["kid"]) assert result["OK"], result["Message"] # as_dict has no arguments for authlib < 1.0.0 # for authlib >= 1.0.0: assert result["Value"].as_dict(True) == private_key.as_dict(True) # Sign token token = jwt.encode(header, payload, private_key) # Sign auth code code = jws.serialize_compact(header, json_b64encode(code_payload), private_key) # Get public key set result = db.getKeySet() keyset = result["Value"] assert result["OK"], result["Message"] # as_dict has no arguments for authlib < 1.0.0 # for authlib >= 1.0.0: assert bool([ key for key in keyset.as_dict(True)["keys"] if key["kid"] == header["kid"] ]) # Read token _payload = jwt.decode(token, JsonWebKey.import_key_set(keyset.as_dict())) assert _payload == payload # Read auth code data = jws.deserialize_compact(code, keyset.keys[0]) _code_payload = json_loads(urlsafe_b64decode(data["payload"])) assert _code_payload == code_payload