JWS (JSON Web Signature) using Bitcoin message signing as the algorithm.
By default it's expected that secp256k1 is available, so install it before proceeding; make sure to run ./configure --enable-module-recovery
. If you're using some other library that provides the functionality necessary for this, check the Using a custom library section below.
bitjws can be installed by running pip install bitjws
.
In case you need to install the secp256k1
C library, the following sequence of commands is recommended. If you already have secp256k1
, make sure it was compiled from the expected git commit or it might fail to work due to API incompatibilities.
git clone git://github.com/bitcoin/secp256k1.git libsecp256k1
cd libsecp256k1
git checkout d7eb1ae96dfe9d497a26b3e7ff8b6f58e61e400a
./autogen.sh
./configure --enable-module-recovery
make
sudo make install
Use this package to produce signed JWS messages using the Bitcoin message signing schema and to validate such messages. The JWS header generated is the following one:
{
"typ": "JWT",
"alg": "CUSTOM-BITCOIN-SIGN",
"kid": <bitcoin_address>
}
where "kid" is used to indicate the public part of the key used during signing.
import bitjws
mykey = bitjws.PrivateKey()
data = bitjws.sign_serialize(mykey)
sign_serialize
function definition:
def sign_serialize(privkey, expire_after=3600, requrl=None, **kwargs):
"""
Produce a JWT compact serialization by generating a header, payload, and
signature using the privkey specified.
The parameter expire_after is used by the server to reject the payload
if received after current_time + expire_after. Set it to None to
disable its use.
The parameter requrl is optionally used by the server to reject the
payload if it is not delivered to the proper place, e.g. if requrl is
set to https://example.com/api/login but sent to a different server or
path then the receiving server should reject it.
Any other parameters are passed as is to the payload.
"""
import bitjws
header, payload = bitjws.validate_deserialize(data)
validate_deserialize
may raise bitjws.InvalidMessage
or bitwjs.InvalidPayload
. Function definition:
def validate_deserialize(rawmsg, requrl=None, check_expiration=True,
decode_payload=True):
"""
Validate a JWT compact serialization and return the header and
payload if the signature is good.
If check_expiration is False, the payload will be accepted even if
expired.
If decode_payload is True then this function will attempt to decode
it as JSON, otherwise the raw payload will be returned. Note that
it is always decoded from base64url.
"""
import bitjws
key1 = bitjws.PrivateKey()
key2 = bitjws.PrivateKey()
data = bitjws.multisig_sign_serialize([key1, key2])
headers, payload = bitjws.multisig_validate_deserialize(data)
The other parameters accepted by multisig_sign_serialize
and multisig_validate_deserialize
are the same as described for sign_serialize
and validate_deserialize
. The data returned and passed to the validate function are different, as the multisig functions use the format described as general JSON serialization in the JWS spec.
Check tests/
and example/
for other functions available but not documented above.
It's possible to use bitjws
without the secp256k1
library, as well with other signing algorithms.
To install bitjws
without secp256k1
, use:
pip install bitjws --no-deps
pip install base58
bitjws
allows custom algorithms to be registered. They are used during signing/validation and are assumed to be an instance of bitjws.Algorithm
.
First define a new implementation:
algorithm = bitjws.Algorithm(name,
sign=sign_function,
verify=verify_function,
pubkey_serialize=pubkey_serialize_function)
And then register it:
bitjws.ALGORITHM_AVAILABLE[algorithm.name] = algorithm
To successfully use this algorithm, the following expectations must be met:
sign_function
takes a private key and data to be signed and returns bytes.verify_function
takes a signature, the original data, and an address (the Bitcoin address or something equivalent for another implementation, like a public key) and returns a boolean (True if verification is successfull, False otherwise).- The
pubkey_serialize_function
function takes a single parameter (e.g. a public key) and returns text (e.g. a bitcoin address). - The private key has a member named
pubkey
.
Now it's possible to call the sign/validate functions with the parameter algorithm_name=algorithm.name
.
Run pip install python-bitcoinlib
if you don't have this custom dependency installed. The following snippet registers a new algorithm as mentioned above and uses a sample key for a complete example.
import bitjws
from bitcoin.wallet import CBitcoinSecret, P2PKHBitcoinAddress
from bitcoin.signmessage import BitcoinMessage, VerifyMessage, SignMessage
# Compatibility functions.
def sign(privkey, data):
return SignMessage(privkey, BitcoinMessage(data))
def verify(sig, data, address):
return VerifyMessage(address, BitcoinMessage(data), sig)
def pubkey_serialize(pubkey):
return str(P2PKHBitcoinAddress.from_pubkey(pubkey))
# Register algorithm.
algo = bitjws.Algorithm('CUSTOM-BITCOIN-SIGN',
sign=sign, verify=verify, pubkey_serialize=pubkey_serialize)
bitjws.ALGORITHM_AVAILABLE[algo.name] = algo
# bitjws expects privkey objects to contain a pubkey member.
key = CBitcoinSecret("L4vB5fomsK8L95wQ7GFzvErYGht49JsCPJyJMHpB4xGM6xgi2jvG")
key.pubkey = key.pub
# sign/verify using the algorithm registered.
ser = bitjws.sign_serialize(key, hello='world', algorithm_name=algo.name)
print(ser)
headers, payload = bitjws.validate_deserialize(ser, algorithm_name=algo.name)
print(headers, payload)
assert headers['kid'] == '1F26pNMrywyZJdr22jErtKcjF8R3Ttt55G'
Key input | Serialization output |
---|---|
import bitjws
rawkey = b'\x01' * 32
key = bitjws.PrivateKey(rawkey) |
ser = bitjws.sign_serialize(key, expire_after=None) |
eyJhbGciOiAiQ1VTVE9NLUJJVENPSU4tU0lHTiIsICJraWQiOiAiMUM2UmM zdzI1Vkh1ZDNkTERhbXV0YXFmS1dxaHJMUlRhRCIsICJ0eXAiOiAiSldUIn0. eyJhdWQiOiBudWxsLCAiZXhwIjogMjE0NzQ4MzY0OH0. SUptY1VJZXBrSllZMFpxS0FVcStNOUVjK0tWSitUUG13c0MrREMveXhOc0N LRXIvbzJNd3NoMWRubGdsRnI0ZjdrSFQrZ1ZkL25IUkFRMEpDdGx6S0VjPQ |
Line breaks were added in the serialization output, but none of those are present. There are three segments separated by ".": header, payload, and signature, respectively. The segments can be separated by performing header, payload, signature = ser.split('.')
.
Raw header | Decoded header |
---|---|
eyJhbGciOiAiQ1VTVE9NLUJJVENPSU4tU0lHTiIsICJraWQiOiAiMUM2UmMzdz I1Vkh1ZDNkTERhbXV0YXFmS1dxaHJMUlRhRCIsICJ0eXAiOiAiSldUIn0 |
bitjws.base64url_decode(header.encode('utf8')) |
{ "alg": "CUSTOM-BITCOIN-SIGN", "kid": "1C6Rc3w25VHud3dLDamutaqfKWqhrLRTaD", "typ": "JWT" } |
Raw payload | Decoded payload |
---|---|
eyJhdWQiOiBudWxsLCAiZXhwIjogMjE0NzQ4MzY0OH0 | bitjws.base64url_decode(payload.encode('utf8')) |
{ "aud": null, "exp": 2147483648 } |
Raw signature | Decoded signature |
---|---|
SUptY1VJZXBrSllZMFpxS0FVcStNOUVjK0tWSitUUG13c0MrREMveXhOc0N LRXIvbzJNd3NoMWRubGdsRnI0ZjdrSFQrZ1ZkL25IUkFRMEpDdGx6S0VjPQ |
bitjws.base64url_decode(
signature.encode('utf8')) |
IJmcUIepkJYY0ZqKAUq+M9Ec+KVJ+TPmwsC+DC/yxNs CKEr/o2Mwsh1dnlglFr4f7kHT+gVd/nHRAQ0JCtlzKEc= |
There are no line breaks in the decoded signature, they were added to make it easier to notice the different segments. The decoded signature is the base64 signature produced according to the Bitcoin message signing method.
Using the same key from the previous section, running bitjws.multisig_sign_serialize([key], expire_after=None)
resuts in the following output:
{
"payload": "eyJhdWQiOiBudWxsLCAiZXhwIjogMjE0NzQ4MzY0OH0",
"signatures": [
{
"signature": "SUptY1VJZXBrSllZMFpxS0FVcStNOUVjK0tWSitUUG13c0MrREMveXhOc0NLRXIvbzJNd3NoMWRubGdsRnI0ZjdrSFQrZ1ZkL25IUkFRMEpDdGx6S0VjPQ",
"protected": "eyJhbGciOiAiQ1VTVE9NLUJJVENPSU4tU0lHTiIsICJraWQiOiAiMUM2UmMzdzI1Vkh1ZDNkTERhbXV0YXFmS1dxaHJMUlRhRCIsICJ0eXAiOiAiSldUIn0"
}
]
}
This is a different format from the one used for single key signing. This format is defined as "general JSON serialization" in the JWS spec, and is used to store a list of signatures and headers. The headers are stored in the "protected" fields, which means their values are integrity protected (i.e. the signature takes them into account). Decoding the values for payload
, signatures[0]["signature"]
, signatures[0]["protected"]
is done using the same bitjws.base64url_decode
function used earlier. The number of signatures corresponds to the number of keys passed to bitjws.multisig_sign_serialize
.