def test_encode_decode(): key1 = bitjws.PrivateKey() pubkey1 = bitjws.pubkey_to_addr(key1.pubkey.serialize()) key2 = bitjws.PrivateKey() pubkey2 = bitjws.pubkey_to_addr(key2.pubkey.serialize()) ser = bitjws.multisig_sign_serialize([key1, key2]) headers, payload = bitjws.multisig_validate_deserialize(ser) rawpayload = json.loads(ser)['payload'] origpayload = bitjws.base64url_decode(rawpayload.encode('utf8')) keys_found = {pubkey1: False, pubkey2: False} assert len(headers) == 2 for h in headers: assert len(h) == 3 assert h['typ'] == 'JWT' assert h['alg'] == 'CUSTOM-BITCOIN-SIGN' assert h['kid'] in keys_found assert keys_found[h['kid']] == False keys_found[h['kid']] = True assert all(keys_found.values()) assert isinstance(payload.get('exp', ''), (float, int)) assert payload['aud'] is None assert len(payload) == 2 assert payload == json.loads(origpayload.decode('utf8'))
def test_bad_signature(): wif = 'L2Ai1TBwKfyPshmqosKRBvJ47qUCDKesfZXh2zLoYoB7NHgdPS6d' key = bitjws.PrivateKey(bitjws.wif_to_privkey(wif)) assert bitjws.privkey_to_wif(key.private_key) == wif ser = bitjws.sign_serialize(key) # Drop the last byte from the signature. ser = ser[:-1] with pytest.raises(bitjws.jws.InvalidMessage): # It will fail to decode as base64 due to padding. bitjws.validate_deserialize(ser) # Drop another byte. ser = ser[:-1] with pytest.raises(bitjws.jws.InvalidMessage): # Although it can be decoded now, the length is incorrect. bitjws.validate_deserialize(ser) # Replace the signature by something that has the correct # length before decoding but becomes invalid after it. dummy = bitjws.base64url_encode(b'a' * 88) ser = ser[:ser.rfind('.')] + '.' + dummy.decode('utf8') with pytest.raises(bitjws.jws.InvalidMessage): # Now it fails because the dummy signature above produces # 66 bytes (instead of 65) after being decoded. bitjws.validate_deserialize(ser)
def test_no_expire(): key = bitjws.PrivateKey() ser = bitjws.sign_serialize(key, expire_after=None) header, payload = bitjws.validate_deserialize(ser) assert header is not None assert payload['exp'] >= 2 ** 31
def _load_config(self, config): if ENVCFG in os.environ: logging.info("Loading config from {}".format(ENVCFG)) #self.config.from_envvar(ENVCFG) data = json.load(open(os.getenv(ENVCFG))) self.config.update(**data) else: logging.warn("{} not defined".format(ENVCFG)) if isinstance(config, dict): self.config.update(**config) if not self.config.get('api_signing_key'): raise Exception("Invalid config: missing api_signing_key") # Server key used to sign responses. The client can optionally # check that the response was signed by a key it knows to belong # to this server. self._privkey = bitjws.PrivateKey( bitjws.wif_to_privkey(self.config['api_signing_key'])) logging.info("Server key address: {}".format( bitjws.pubkey_to_addr(self._privkey.pubkey.serialize()))) self._cosigner_server = self.config.get('cosigner_server') if not self._cosigner_server: logging.warning("cosigner_server not present in config, " "cosigning will not be available.") self._dbcfg = self.config.get('api_database') if not self._dbcfg: raise Exception("Invalid config: missing api_database") if 'engine' not in self._dbcfg: raise Exception("Invalid config for api_databae: missing 'engine'")
def test_init_basepath(): privkey = bitjws.PrivateKey(bitjws.wif_to_privkey(wif)) fbj = FlaskBitjws(server.app, privkey=privkey, basepath="/v3") app = server.app.test_client() import time user = app.post('/echo', data={'username': str(time.time)}) assert user.status_code == 401
def test_no_nonce(): app = server.app.test_client() privkey = bitjws.PrivateKey() echo_msg = {'hello': 'server'} data = bitjws.sign_serialize(privkey, echo=echo_msg, requrl="/echo") echo = app.post('/echo', data=data, headers={'Content-Type': 'application/jose'}) assert echo.status_code == 401
def test_dummy_algo(): key = bitjws.PrivateKey() # Setup a new algorithm for signing/verifying. # The sign function always take two parameters and return bytes. # The verify function always take three parameters and return a boolean. # The pubkey_serialize function take a pubkey object and return text. newalgo = bitjws.Algorithm( 'dummy', sign=lambda privkey, signdata: signdata.encode('utf8'), verify=lambda sig, data, pubkey: True, pubkey_serialize=lambda x: "hello") bitjws.ALGORITHM_AVAILABLE[newalgo.name] = newalgo ser = bitjws.sign_serialize(key) with pytest.raises(bitjws.InvalidMessage): # Algorithm mismatch. bitjws.validate_deserialize(ser, algorithm_name='dummy') ser = bitjws.sign_serialize(key, algorithm_name='dummy') with pytest.raises(bitjws.InvalidMessage): # Algorithm mismatch. bitjws.validate_deserialize(ser) result = bitjws.validate_deserialize(ser, algorithm_name='dummy') assert result[0] and result[1]
def test_payload_nojson(): key = bitjws.PrivateKey() # Use a payload that is not JSON encoded. ser = json.loads(bitjws.multisig_sign_serialize([key])) ser['payload'] = bitjws.base64url_encode(b'test').decode('utf8') # Sign the new payload. signdata = '{}.{}'.format(ser['signatures'][0]['protected'], ser['payload']) sig = bitjws.ALGORITHM_AVAILABLE['CUSTOM-BITCOIN-SIGN'].sign(key, signdata) sig64 = bitjws.base64url_encode(sig).decode('utf8') ser['signatures'][0]['signature'] = sig64 serenc = json.dumps(ser) with pytest.raises(bitjws.InvalidMessage): # The new payload was not JSON encoded, so it cannot be # decoded as that. bitjws.multisig_validate_deserialize(serenc) # But we can get its raw value. headers, payload = bitjws.multisig_validate_deserialize( serenc, decode_payload=False) assert len(headers) == 1 assert payload == b'test'
def test_payload_expired(): key = bitjws.PrivateKey() print(bitjws.privkey_to_wif(key.private_key)) # expire_after must be greater than 0 or None. with pytest.raises(bitjws.jws.InvalidPayload): bitjws.sign_serialize(key, expire_after=0) ser = bitjws.sign_serialize(key, expire_after=0.01) time.sleep(0.02) with pytest.raises(bitjws.jws.InvalidPayload): # payload expired. bitjws.validate_deserialize(ser) # But the signature can still be verified and the msg decoded # if expiration checks are disabled. h, p = bitjws.validate_deserialize(ser, check_expiration=False) assert h and p # Check with multisig. ser = bitjws.multisig_sign_serialize([key], expire_after=0.01) time.sleep(0.02) with pytest.raises(bitjws.jws.InvalidPayload): # payload expired. bitjws.multisig_validate_deserialize(ser)
def test_encode_decode(): key = bitjws.PrivateKey() ser = bitjws.sign_serialize(key) header, payload = bitjws.validate_deserialize(ser) rawheader, rawpayload = ser.rsplit('.', 1)[0].split('.') origheader = bitjws.base64url_decode(rawheader.encode('utf8')) origpayload = bitjws.base64url_decode(rawpayload.encode('utf8')) assert header['typ'] == 'JWT' assert header['alg'] == 'CUSTOM-BITCOIN-SIGN' assert header['kid'] == bitjws.pubkey_to_addr(key.pubkey.serialize()) assert len(header) == 3 assert header == json.loads(origheader.decode('utf8')) assert isinstance(payload.get('exp', ''), (float, int)) assert payload['aud'] is None assert len(payload) == 2 assert payload == json.loads(origpayload.decode('utf8')) # Assumption: it takes mores than 0 seconds to perform the above # instructions but less than 1 second. 3600 is the default # expiration time. diff = 3600 - (payload['exp'] - time.time()) assert diff > 0 and diff < 1
def test_multisig_missingkeys(): key = bitjws.PrivateKey() ser = bitjws.multisig_sign_serialize([key]) # This should work. headers, payload = bitjws.multisig_validate_deserialize(ser) assert len(headers) == 1 and payload serobj = json.loads(ser) payload = serobj.pop('payload') ser = json.dumps(serobj) with pytest.raises(bitjws.jws.InvalidMessage): # Key 'payload' is missing. bitjws.multisig_validate_deserialize(ser) serobj['payload'] = payload signatures = serobj.pop('signatures') ser = json.dumps(serobj) with pytest.raises(bitjws.jws.InvalidMessage): # Key 'signatures' is missing. bitjws.multisig_validate_deserialize(ser) serobj['signatures'] = 'hello' ser = json.dumps(serobj) with pytest.raises(bitjws.jws.InvalidMessage): # 'signatures' is not a list. bitjws.multisig_validate_deserialize(ser) del serobj['signatures'] del serobj['payload'] ser = json.dumps(serobj) with pytest.raises(bitjws.jws.InvalidMessage): bitjws.multisig_validate_deserialize(ser) # Remove a key from one of the signatures. serobj['signatures'] = signatures serobj['payload'] = payload sig0 = serobj['signatures'][0].copy() del serobj['signatures'][0]['protected'] ser = json.dumps(serobj) with pytest.raises(bitjws.jws.InvalidMessage): # data['signatures'][0]['protected'] is missing bitjws.multisig_validate_deserialize(ser) serobj['signatures'][0] = sig0.copy() del serobj['signatures'][0]['signature'] ser = json.dumps(serobj) with pytest.raises(bitjws.jws.InvalidMessage): # data['signatures'][0]['signture'] is missing bitjws.multisig_validate_deserialize(ser) # Remove the only signature entry. serobj['signatures'].pop() ser = json.dumps(serobj) with pytest.raises(bitjws.jws.InvalidMessage): # No signatures. bitjws.multisig_validate_deserialize(ser)
def test_audience(): key = bitjws.PrivateKey() audience = 'https://example.com/api/login' ser = bitjws.sign_serialize(key, requrl=audience) header, payload = bitjws.validate_deserialize(ser, requrl=audience) assert header is not None assert payload is not None assert payload['aud'] == audience
def test_too_big(): key = bitjws.PrivateKey(rawkey) try: ser = bitjws.sign_serialize(key, test='a' * 4294967295) h, p = bitjws.validate_deserialize(ser) assert h and p except MemoryError: pytest.skip('Not enough memory to run this test') return
def test_multisig_partiallyinvalid(): key1 = bitjws.PrivateKey() key2 = bitjws.PrivateKey() key3 = bitjws.PrivateKey() ser = bitjws.multisig_sign_serialize([key1, key2, key3]) assert all(bitjws.multisig_validate_deserialize(ser)) serobj = json.loads(ser) # Swap signatures. sig0 = serobj['signatures'][0]['signature'] sig1 = serobj['signatures'][1]['signature'] sig0, sig1 = sig1, sig0 serobj['signatures'][0]['signature'] = sig0 serobj['signatures'][1]['signature'] = sig1 ser = json.dumps(serobj) headers, payload = bitjws.multisig_validate_deserialize(ser) assert headers is None assert payload is None
def test_bad_request(): app = server.app.test_client() privkey = bitjws.PrivateKey() echo_msg = {'hello': 'server'} data = bitjws.sign_serialize(privkey, echo=echo_msg, iat=time.time(), requrl="/echo") data2 = bitjws.sign_serialize(privkey, echo='not%s' % echo_msg) da = data.split('.') da2 = data2.split('.') baddata = "%s.%s.%s" % (da[0], da2[1], da[2]) echo = app.post('/echo', data=baddata, headers={'Content-Type': 'application/jose'}) assert echo.status_code == 401
def test_invalid_audience(): key = bitjws.PrivateKey() print(bitjws.privkey_to_wif(key.private_key)) ser = bitjws.sign_serialize(key, requrl='https://example.com/api/login') with pytest.raises(bitjws.jws.InvalidPayload): # audience not specified. bitjws.validate_deserialize(ser) with pytest.raises(bitjws.jws.InvalidPayload): # audience does not match. bitjws.validate_deserialize(ser, requrl='https://example.com/api')
def test_register_user(): # register a new user privkey2 = bitjws.PrivateKey() my_pubkey2 = privkey2.pubkey.serialize() my_address2 = bitjws.pubkey_to_addr(my_pubkey2) username2 = str(my_address2)[0:8] client2 = BitJWSSwaggerClient.from_url(specurl, privkey=privkey2) luser2 = client2.get_model('User')(username=username2) user2 = client2.user.addUser(user=luser2).result() assert hasattr(user2, 'id')
def __init__(self, app, privkey=None, loginmanager=None, get_last_nonce=get_last_nonce, get_user_by_key=get_user_by_key, basepath=""): """ Initialize a flask-bitjws Application with optional LoginManager. If you do not provide your own LoginManager one will be created. Create or set the private key to use with this application. :param str app: The flask application :param str privkey: the bitjws private key to use for signing responses :param flask.ext.login.LoginManager loginmanager: An optional LoginManager :param function get_last_nonce: A function to overwrite this class's stub. :param function get_user_by_key: A function to overwrite this class's stub. :param str basepath: The public basepath this app is deployed on. (only preceding slash required i.e. '/path') """ if privkey is not None and isinstance(privkey, str): self._privkey = bitjws.PrivateKey(bitjws.wif_to_privkey(privkey)) elif privkey is not None and isinstance(privkey, bitjws.PrivateKey): self._privkey = privkey else: self._privkey = bitjws.PrivateKey() self.pubkey = bitjws.pubkey_to_addr(self._privkey.pubkey.serialize()) if loginmanager is None: loginmanager = login.LoginManager() loginmanager.anonymous_user = login.AnonymousUserMixin loginmanager.init_app(app) loginmanager.request_loader(load_user_from_request) self.get_last_nonce = get_last_nonce self.get_user_by_key = get_user_by_key self.basepath = basepath app.bitjws = self
def test_bad_response(): app = server.app.test_client() privkey = bitjws.PrivateKey() echo_msg = {'hello': 'server'} data = bitjws.sign_serialize(privkey, echo=echo_msg, iat=time.time(), requrl="/echo") data2 = bitjws.sign_serialize(privkey, echo='not%s' % echo_msg) da = data.split('.') da2 = data2.split('.') baddata = "%s.%s.%s" % (da[0], da2[1], da[2]) echo = app.post('/echo', data=data, headers={'Content-Type': 'application/jose'}) echo.data = baddata h, p = bitjws.validate_deserialize(echo.get_data().decode('utf8'), requrl='/response') assert h is None
def test_multisig_invalidsig(): key = bitjws.PrivateKey() ser = bitjws.multisig_sign_serialize([key]) assert all(bitjws.multisig_validate_deserialize(ser)) serobj = json.loads(ser) dummy = bitjws.base64url_encode(b'a' * 88) serobj['signatures'][0]['signature'] = dummy.decode('utf8') ser = json.dumps(serobj) with pytest.raises(bitjws.jws.InvalidMessage): # Invalid signature length. bitjws.multisig_validate_deserialize(ser)
def test_invalid_algorithm(): key1 = bitjws.PrivateKey() # Invalid algorithm name for signing. with pytest.raises(AssertionError): bitjws.sign_serialize(key1, algorithm_name='test') with pytest.raises(AssertionError): bitjws.multisig_sign_serialize([key1], algorithm_name='test') # Invalid algorithm name for verifying. ser = bitjws.sign_serialize(key1) with pytest.raises(AssertionError): bitjws.validate_deserialize(ser, algorithm_name='test') ser = bitjws.multisig_sign_serialize([key1]) with pytest.raises(AssertionError): bitjws.multisig_validate_deserialize(ser, algorithm_name='test')
def test_echo_request(): privkey = bitjws.PrivateKey() pubkey = bitjws.pubkey_to_addr(privkey.pubkey.serialize()) echo_msg = {'hello': 'server'} data = bitjws.sign_serialize(privkey, data=echo_msg, iat=time.time(), requrl="/echo") fbj = FlaskBitjws(server.app) app = server.app.test_client() udata = json.dumps({'username': pubkey[0:8], 'kid': pubkey}) user = app.post('/user', data=udata) echo = app.post('/echo', data=data, headers={'Content-Type': 'application/jose'}) h, p = bitjws.validate_deserialize(echo.get_data().decode('utf8'), requrl='/response') assert 'alg' in h assert h['alg'] == 'CUSTOM-BITCOIN-SIGN' assert 'typ' in h assert 'kid' in h assert 'data' in p assert p['data'] == echo_msg
def test_invalid_signature_key(): key = bitjws.PrivateKey() print(bitjws.privkey_to_wif(key.private_key)) ser = bitjws.sign_serialize(key) # Decode header. rawheader = ser.rsplit('.')[0] origheader = bitjws.base64url_decode(rawheader.encode('utf8')) header = json.loads(origheader.decode('utf8')) # Modify the key declared to be used in the signature. header['kid'] = '123' ser = _encode_header(header, ser) header, payload = bitjws.validate_deserialize(ser) # If both header or payload are None then it failed to validate # the signature (as expected). assert header is None assert payload is None
def test_none_algo(): # Register an algorithm named None. newalgo = bitjws.Algorithm(None, sign=lambda a, b: b.encode('utf8'), verify=lambda a, b, c: True, pubkey_serialize=lambda x: "hello") bitjws.ALGORITHM_AVAILABLE[None] = newalgo key = bitjws.PrivateKey() ser = bitjws.sign_serialize(key, algorithm_name=None) with pytest.raises(bitjws.InvalidMessage): # Although it "signs" a message, decoding does complete # due to its name (None). bitjws.validate_deserialize(ser, algorithm_name=None) with pytest.raises(bitjws.InvalidMessage): # The algorithm name (None) is defined in the header, so it # fails to decode here too. bitjws.validate_deserialize(ser)
def test_invalid_header(): key = bitjws.PrivateKey() print(bitjws.privkey_to_wif(key.private_key)) ser = bitjws.sign_serialize(key) # Decode header. rawheader = ser.split('.')[0] origheader = bitjws.base64url_decode(rawheader.encode('utf8')) header = json.loads(origheader.decode('utf8')) # Modify the algorithm specified (by removing it). algorithm = header.pop('alg') # Encode header and try to deserialize. ser = _encode_header(header, ser) with pytest.raises(bitjws.jws.InvalidMessage): # Unknown algorithm. bitjws.validate_deserialize(ser) # Set some other algorithm. header['alg'] = 'SHA256' ser = _encode_header(header, ser) with pytest.raises(bitjws.jws.InvalidMessage): # Unknown algorithm. bitjws.validate_deserialize(ser) # Drop the key used to sign. header['alg'] = algorithm kid = header.pop('kid') ser = _encode_header(header, ser) with pytest.raises(bitjws.jws.InvalidMessage): # No address specified. bitjws.validate_deserialize(ser) # Try to decode the original one. ser = rawheader + '.' + ser.split('.', 1)[1] header, payload = bitjws.validate_deserialize(ser) assert header is not None assert payload is not None h, p = bitjws.validate_deserialize(ser, check_expiration=False) assert h == header assert p == payload
def test_malformed(): key = bitjws.PrivateKey() print(bitjws.privkey_to_wif(key.private_key)) ser = bitjws.sign_serialize(key) # Drop one of the segments. ser = ser.split('.', 1)[1] with pytest.raises(bitjws.jws.InvalidMessage): bitjws.validate_deserialize(ser) # Add an empty segment. ser = '.' + ser with pytest.raises(bitjws.jws.InvalidMessage): bitjws.validate_deserialize(ser) # Add an invalid segment. ser = ' ' + ser with pytest.raises(bitjws.jws.InvalidMessage): # This will fail while trying to parse it as JSON. bitjws.validate_deserialize(ser)
def test_payload_nojson(): key = bitjws.PrivateKey() ser = bitjws.sign_serialize(key) header64 = ser.split('.')[0] # Sign a new payload, 'test' new_payload = bitjws.base64url_encode(b'test').decode('utf8') signdata = '{}.{}'.format(header64, new_payload) sig = bitjws.ALGORITHM_AVAILABLE['CUSTOM-BITCOIN-SIGN'].sign( key, signdata) sig64 = bitjws.base64url_encode(sig).decode('utf8') new = '{}.{}'.format(signdata, sig64) with pytest.raises(bitjws.InvalidMessage): # The new payload was not JSON encoded, so it cannot be # decoded as that. bitjws.validate_deserialize(new) # But we can get its raw value. header, payload = bitjws.validate_deserialize(new, decode_payload=False) assert header is not None assert payload == b'test'
def test_big(): key = bitjws.PrivateKey() ser = bitjws.sign_serialize(key, test='a' * 65536) h, p = bitjws.validate_deserialize(ser) assert h and p
# Install requirements: pip install flask flask-login import flask from flask.ext import login import bitjws # Server key used to sign responses. The client can optionally # check that the response was signed by a key it knows to belong # to this server. privkey = bitjws.PrivateKey() print("Server WIF key: {}".format(bitjws.privkey_to_wif(privkey.private_key))) print("Server key address: {}".format( bitjws.pubkey_to_addr(privkey.pubkey.serialize()))) # Setup flask app with flask-login. app = flask.Flask(__name__) login_manager = login.LoginManager() login_manager.init_app(app) # Dummy User. class User(login.UserMixin): def __init__(self, address): self.id = address # Custom request authentication based on bitjws. @login_manager.request_loader def load_user_from_request(request): header, payload = bitjws.validate_deserialize(request.data.decode('utf8')) if header is None: # Validation failed.
def test_init_PrivateKey(): privkey = bitjws.PrivateKey(bitjws.wif_to_privkey(wif)) app = Flask(__name__) fbj = FlaskBitjws(app, privkey=privkey) assert wif == bitjws.privkey_to_wif(app.bitjws._privkey.private_key)