I am writing this python code to create RSA private and public keys. Then create a JWK from the private key, then sign the claims with that JWK.
#!/usr/bin/env python
import time
from jose import jwk
from jose import jws
from cryptography.hazmat.primitives import serialization as crypto_serialization
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.backends import default_backend as crypto_default_backend
key = rsa.generate_private_key(backend=crypto_default_backend(), public_exponent=65537, key_size=2048)
private_key = key.private_bytes(crypto_serialization.Encoding.PEM, crypto_serialization.PrivateFormat.PKCS8, crypto_serialization.NoEncryption())
public_key = key.public_key().public_bytes(crypto_serialization.Encoding.OpenSSH, crypto_serialization.PublicFormat.OpenSSH)
key = jwk.construct(private_key, 'RS256')
print 'key.to_dict() = {}'.format(key.to_dict())
claims = {
'iss': 'https://e97b8a9d672e4ce4845ec6947cd66ef6-sb.baas.nintendo.com',
'sub': 'fdfdc610f849726e',
'aud': '20c875ad0d4bfc94',
'iat': time.time() - 20,
'exp': time.time() + 20,
'jti': '807443d3-3b27-4bf9-8e3e-e3f90e1ea055',
'typ': 'id_token'
}
print 'About to sign'
signed = jws.sign(claims, key, algorithm='RS256')
When I run it, it fails at the signing step:
key.to_dict() = {
'e': 'AQAB',
'kty': 'RSA',
'alg': 'RS256',
'n': 'uJ1_BLAH_BLAH_BLAH_veQ',
'q': '8Sa_BLAH_BLAH_BLAH_-Hs',
'p': 'w_t_BLAH_BLAH_BLAH_p5s',
'qi': 'OGz_BLAH_BLAH_BLAH_91U',
'dq': 'D2n_BLAH_BLAH_BLAH_5FM',
'dp': 'pDi_BLAH_BLAH_BLAH_J2k',
'd': 'oV0_BLAH_BLAH_BLAH_VLQ'
}
About to sign
Traceback (most recent call last):
File "./my_file.py", line 56, in <module>
signed = jws.sign(claims, key, algorithm='RS256')
File "my-virtual-env/lib/python2.7/site-packages/jose/jws.py", line 47, in sign
signed_output = _sign_header_and_claims(encoded_header, encoded_payload, algorithm, key)
File "my-virtual-env/lib/python2.7/site-packages/jose/jws.py", line 168, in _sign_header_and_claims
raise JWSError(e)
jose.exceptions.JWSError: Unable to parse an RSA_JWK from key: <jose.backends.pycrypto_backend.RSAKey object at 0x101761190>
What is causing this error and how can I make this work??
Using RS256, you need to give the private key to jws.sign(), but not with the JWK format.
Therefore, just replace
signed = jws.sign(claims, key, algorithm='RS256')
by
signed = jws.sign(claims, private_key, algorithm='RS256')
And it will work correctly.
Finally, the whole source is:
#!/usr/bin/env python
import time
from jose import jws
from cryptography.hazmat.primitives import serialization as crypto_serialization
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.backends import default_backend as crypto_default_backend
key = rsa.generate_private_key(backend=crypto_default_backend(), public_exponent=65537, key_size=2048)
private_key = key.private_bytes(crypto_serialization.Encoding.PEM, crypto_serialization.PrivateFormat.PKCS8, crypto_serialization.NoEncryption())
claims = {
'iss': 'https://e97b8a9d672e4ce4845ec6947cd66ef6-sb.baas.nintendo.com',
'sub': 'fdfdc610f849726e',
'aud': '20c875ad0d4bfc94',
'iat': time.time() - 20,
'exp': time.time() + 20,
'jti': '807443d3-3b27-4bf9-8e3e-e3f90e1ea055',
'typ': 'id_token'
}
print 'About to sign'
signed = jws.sign(claims, private_key, algorithm='RS256')
print signed
The result is something like:
About to sign
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJmZGZkYzYxMGY4NDk3MjZlIiwiaXNzIjoiaHR0cHM6Ly9lOTdiOGE5ZDY3MmU0Y2U0ODQ1ZWM2OTQ3Y2Q2NmVmNi1zYi5iYWFzLm5pbnRlbmRvLmNvbSIsImp0aSI6IjgwNzQ0M2QzLTNiMjctNGJmOS04ZTNlLWUzZjkwZTFlYTA1NSIsImV4cCI6MTU0NTkzNTQ1Mi4wMzAxMTUsImlhdCI6MTU0NTkzNTQxMi4wMzAxMTMsInR5cCI6ImlkX3Rva2VuIiwiYXVkIjoiMjBjODc1YWQwZDRiZmM5NCJ9.Qfmi607XTtq8bc4daJ-GxoYy1B761nRahWSok9ga0CB8dqbDXN0FhcM38dNj0LMFoi98hxB9o0EwY1mpBEMA3rNK9C6Bbg1hKhkYE5lRFXH5_lLk2tE6YrbK7p71S54hkTfEZgPGCvkv79HBjUo3PORv6bG-fdQp5UQo8ZMvd5lobmjbMna21JCBp1ITm3QSAPy4gdvfCyzxEziVyRcKwXU45ky3g7wFnyNxr9HtzX6yDJn1LlWdhChNR4uom05U5mbIDSChHAd52nWtzNb6dvdupIKCSR1XN_jF28y1Wu4aMbmVRXAn0X87Qok93KiHMoU_wydWuG5zl-ihqQ-1RA
Related
Hi I have this code from this article
https://medium.com/#alexastrum/firebase-auth-for-iot-devices-286679a8b59e
I was wondering what the equivalent python code would be thanks.
String getDeviceToken()
{
String header = "";
DynamicJsonDocument json(1024);
// ATECCx08 crypto chips only support ES256:
// https://github.com/MicrochipTech/cryptoauthlib/blob/master/lib/jwt/atca_jwt.c
json["alg"] = "ES256";
json["kid"] = DEVICE_ID;
serializeJson(json, header);
String payload;
json.clear();
json["nonce"] = NONCE;
serializeJson(json, payload);
return ECCX08JWS.sign(/* slot */ 0, header, payload);
}
import jwt as jwt
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.asymmetric import ec
# Generates the private key
private_key = ec.generate_private_key(ec.SECP256R1(), default_backend())
# Generates the public key
public_key = private_key.public_key()
jwt.encode(
payload={"nonce": NONCE},
key=private_key,
algorithm="ES256",
headers={"alg": "ES256", "kid": DEVICE_ID},
)
Recently I've been having some trouble with the X-Flashbots-Signature header when sending a request to the flashbots goerli endpoint.
My python code looks like this:
import requests
import json
import secrets
from eth_account import Account, messages
from web3 import Web3
from math import ceil
rpcUrl = GOERLI_RPC_NODE_PROVIDER
web3 = Web3(Web3.HTTPProvider(rpcUrl))
publicKey = ETH_PUBLIC_KEY
privateKey = ETH_PRIVATE_KEY
contractAddress = GOERLI_TEST_CONTRACT # Goerli test contract
data = CONTRACT_DATA # Contract data to execute
signed = []
for _ in range(2):
nonce = web3.eth.getTransactionCount(publicKey, 'pending')
checksumAddress = Web3.toChecksumAddress(contractAddress)
checksumPublic = Web3.toChecksumAddress(publicKey)
tx = {
'nonce': nonce,
'to': checksumAddress,
'from': checksumPublic,
'value': 0,
'gasPrice': web3.toWei(200, 'gwei'),
'data': data
}
gas = web3.eth.estimateGas(tx)
tx['gas'] = ceil(gas + gas * .1)
signed_tx = web3.eth.account.signTransaction(tx, privateKey)
signed.append(Web3.toHex(signed_tx.rawTransaction))
dt = {
'jsonrpc': '2.0',
'method': 'eth_sendBundle',
'params': [
{
'txs': [
signed[0], signed[1] # Signed txs with web3.eth.account.signTransaction
],
'blockNumber': web3.eth.block_number + 1,
'minTimestamp': '0x0',
'maxTimestamp': '0x0',
'revertingTxHashes': []
}
],
'id': 1337
}
pvk = secrets.token_hex(32)
pbk = Account.from_key(pvk).address
body = json.dumps(dt)
message = messages.encode_defunct(text=Web3.keccak(text=body).hex())
signature = pbk + ':' + Account.sign_message(message, pvk).signature.hex()
hd = {
'Content-Type': 'application/json',
'X-Flashbots-Signature': signature,
}
res = requests.post('https://relay-goerli.flashbots.net/', headers=hd, data=body)
print(res.text)
This code is a modified version of code taken straight from the flashbots docs: https://docs.flashbots.net/flashbots-auction/searchers/advanced/rpc-endpoint/#authentication
Upon running this code I get an internal server error error response. At first, I thought the problem might be fixed by replacing text=Web3.keccak(text=body).hex() to hexstr=Web3.keccak(text=body).hex() or primative=Web3.keccak(text=body), as per the definition of messages.encode_defunct: https://eth-account.readthedocs.io/en/stable/eth_account.html#eth_account.messages.encode_defunct. But after making this replacement, I got the error signer address does not equal expected. This is very confusing, especially because I have resolved the message
with the signature myself and the public key does match. But whenever I send it to the flashbots endpoint, I am left with this error.
Any ideas would be greatly appreciated.
I need to take the RSA PSS signatures of a message generated from Python and validate in .NET. But validation in .NET fails.
I have generated the RSA key pair using the following command:
openssl req -x509 -nodes -newkey rsa:4096 -keyout /tmp/certs/private.pem -out /tmp/certs/public.pem -days 365
Python code:
from cryptography import x509
from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding
from nacl.encoding import HexEncoder
def sign(message: str) -> str:
with open("/tmp/certs/private.pem", "rb") as pem:
private_key = serialization.load_pem_private_key(
pem.read(),
password=None,
backend=default_backend(),
)
return HexEncoder.encode(
private_key.sign(
message.encode("utf-8"),
padding.PSS(
mgf=padding.MGF1(hashes.SHA256()), salt_length=padding.PSS.MAX_LENGTH
),
hashes.SHA256(),
)
).decode("utf-8")
def verify(message: str, signature: str) -> bool:
with open("/tmp/certs/public.pem", "rb") as pem:
cert = x509.load_pem_x509_certificate(pem.read(), default_backend())
public_key = cert.public_key()
try:
signature = HexEncoder().decode(signature)
public_key.verify(
signature,
message.encode(),
padding.PSS(
mgf=padding.MGF1(hashes.SHA256()), salt_length=padding.PSS.MAX_LENGTH
),
hashes.SHA256(),
)
return True
except InvalidSignature:
return False
def main():
message = "hello"
signature = sign(message)
print(f"Signature: {signature}")
# print(f"isValidated: {verify(message, signature)}")
if __name__ == '__main__':
main()
C# code targeting .NET 5.0:
using System;
using System.Collections.Generic;
using System.IO;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
namespace HelloWorld
{
internal static class Program
{
private static string ByteArrayToString(IReadOnlyCollection<byte> ba)
{
var hex = new StringBuilder(ba.Count * 2);
foreach (var b in ba)
hex.AppendFormat("{0:x2}", b);
return hex.ToString();
}
private static byte[] StringToByteArray(string hex)
{
var numberChars = hex.Length;
var bytes = new byte[numberChars / 2];
for (var i = 0; i < numberChars; i += 2)
bytes[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16);
return bytes;
}
private static string Sign(string message)
{
var rsa = RSA.Create();
rsa.ImportFromPem(File.ReadAllText("/tmp/certs/private.pem"));
var messageBytes = Encoding.UTF8.GetBytes(message);
var signature = rsa.SignData(messageBytes, HashAlgorithmName.SHA256, RSASignaturePadding.Pss);
return ByteArrayToString(signature);
}
private static bool Verify(string message, string signature)
{
var rsa = RSA.Create();
var cert = X509Certificate.CreateFromCertFile("/tmp/certs/public.pem");
var publicKey = cert.GetPublicKey();
rsa.ImportRSAPublicKey(publicKey, out _);
var messageBytes = Encoding.UTF8.GetBytes(message);
var signatureBytes = StringToByteArray(signature);
return rsa.VerifyData(messageBytes, signatureBytes, HashAlgorithmName.SHA256, RSASignaturePadding.Pss);
}
public static void Main()
{
const string message = "hello";
// var signature = Sign(message);
const string signature = <value_from_python>;
// Console.WriteLine($"Signature is: {signature}");
Console.WriteLine($"isValidated: {Verify(message, signature)}");
}
}
}
The verification fails because both codes use different salt lengths. The Python code explicitly applies the maximum salt length, the C# code defaults to the digest output length. The latter is also defined as PSS default in RFC8017, A.2.3. RSASSA-PSS.
So to fix this
either use the digest output length in the Python code, i.e. 32 (bytes for SHA256),
or in the C# code the maximum salt length: signature length - digest output length - 2 = 512 - 32 - 2 = 478 (for a 4096 bits key).
As far as I know this is not possible with .NET onboard means, but with BouncyCastle and the class Org.BouncyCastle.Crypto.Signers.PssSigner, which also provides constructors for defining the salt length. You can find an example here, last section.
"n": "rKZ-1zdz_CoLekSynOtyWv6cPSSkV28Kb9kZZHyYL-yhkKnH_bHl8OpWiGxQiKP0ulLRIaq1IhSMetkZ8FfXH-iptIDu4lPb8gt0HQYkjcy3HoaKRXBw2F8fJQO4jQ-ufR4l-E0HRqwLywzdtAImNWmju3A4kx8s0iSGHGSHyE4EUdh5WKt-NMtfUPfB5v9_2bC-w6wH7zAEsI5nscMXnvz1u8w7g2_agyhKSK0D9OkJ02w3I4xLMlrtKEv2naoBGerWckKcQ1kBYUh6WASPdvTqX4pcAJi7Tg6jwQXIP1aEq0JU8C0zE3d33kaMoCN3SenIxpRczRzUHpbZ-gk5PQ",
"e": "AQAB",
How can I generate a public key from these values? Preferable via python or Linux programs. I'm sorry if the question is nonsense because of invalid values.
The source is here.
In Python, you can use Python-JOSE
What you got there in your link is a JSON Web Key Set (JWKS), a JSON formated array of JSON Web Keys (JWK).
n and e are the modulus and exponent of a RSA public key.
The function jwk.construct can directly create a key from a JWK. The key can then be used e.g. to verify a JWT (key.verify).
You can also use jwt.decode like shown in the code below and pass the JWK directly, or in PEM format.
from jose import jwk
from jose.utils import base64url_decode
token = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IlctNjduZWt0WVRjOEpWWVBlV0g1c1dlN1JZVm5uMFN5NzQxZjhUT0pfQWMifQ.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.hiKxeC66LIyVKOXjiOk7iScFPy_5-ATw7hEfqGij8sBZmwXAeTPT5BRFYHitFKSXomGqmy_63LLvg4zbhcTTmNf8XIeDAuLsC32soO5woSByisswWHVf8BgxMkI_FPW_oEtEQ8Xv3FL_1rF9j9Oy3jIjgjqhFhXUtsSQWAeuGYH-OQljFwiuO5Bqexcw-H71OEWvQLQof_6KJ0viJyte8QEwEVridyO834-ppHzeaoW2sTvZ22ZNfxPCew0Ul2V_TxHTtO7ZuJCZ81EmeIV6dYJ2GrYh3UN1x1PHy4-tEn-PL4otlaO3PYOcXfCHxHa6xtPsquzPZJnB1Vq8zULLfQ"
rsa_key = {
"kty": "RSA",
"kid": "W-67nektYTc8JVYPeWH5sWe7RYVnn0Sy741f8TOJ_Ac",
"use": "sig",
"alg": "RS256",
"n": "kFpGoVmBmmKepvBQiwq3hU9lIAuGsAPda4AVk712d3Z_QoS-5veGp4yltnyEFYyX867GOKDpbH7OF2uIjDg4-FPZwbuhiMscbkZzh25SQmfRtCT5ocUloQiopBcNAE-sd1p-ayUJWjhPrFoBrBLZHYxVEjY4JrWevQDj7kSeX7eJpud_VuZ77TNoIzj7d_iUuJUUlqF1ZF540igHKoVJJ6ujQLHh4ob8_izUuxX2iDq4h0VN3-uer59GsWw6OHgkOt85TsjMwYbeN9iw_7cNfLEYpSiH-sVHBCyKYQw7f8bKaChLxDRhUUTIEUUjGT9Ub_A3gOXq9TIi8BmbzrzVKQ",
"e": "AQAB"
}
key = jwk.construct(rsa_key)
message, encoded_sig = token.rsplit('.', 1)
decoded_sig = base64url_decode(encoded_sig + '=' * (4 - len(encoded_sig) % 4)) # looks weird, but without added padding I got errors
res = key.verify(bytes(message, "UTF-8"), decoded_sig)
# jwt.decode(token=token, key=key.to_pem().decode(), algorithms= 'RS256') # with PEM key
payload = jwt.decode(token=token, rsa_key, algorithms= 'RS256') # with JWK
print(res)
print(payload)
The result will be:
True
{'sub': '1234567890', 'name': 'John Doe', 'admin': True, 'iat': 1516239022}
which means the token could be verified with that key.
This is what i have try:
import time
import json
import hashlib
import hmac
from urllib.parse import urlparse
import requests
from datetime import datetime, timedelta
from pprint import pprint
def generate_signature(secret, verb, url, expires, data):
"""Generate a request signature compatible with BitMEX."""
# Parse the url so we can remove the base and extract just the path.
parsedURL = urlparse(url)
path = parsedURL.path
if parsedURL.query:
path = path + '?' + parsedURL.query
if isinstance(data, (bytes, bytearray)):
data = data.decode('utf8')
print("Computing HMAC: %s" % verb + path + str(expires) + data)
message = verb + path + str(expires) + data
signature = hmac.new(bytes(secret, 'utf8'), bytes(message, 'utf8'), digestmod=hashlib.sha256).hexdigest()
return signature
# Or you might generate it like so:
# expires = int(round(time.time()) + 5)
data = {
'filter': {"symbol": "XBTUSD"},
'columns': [],
'count': 100
'start': 1,
'reverse': "true",
'startTime': str(datetime.now() - timedelta(minutes=50)),
'endTime': str(datetime.now())
}
verb = 'GET'
path = '/api/v1/position'
time_in_epoch = int(datetime.timestamp(datetime.now() + timedelta(hours=.5)))
print("========",time_in_epoch, int(round(time.time()) + 1800))
expires = time_in_epoch
gen_signat = generate_signature('api-sceret', verb, path, expires, json.dumps(data))
URL_get_data = "http://www.bitmex.com/api/v1/position"
headers = {
'api-expires': str(expires),
'api-key': "api-key",
'api-signature': str(gen_signat),
}
response = requests.get(URL_get_data, headers=headers, data= data).json()
Still showing error:
{'error': {'message': 'Signature not valid.', 'name': 'HTTPError'}}
If I pass this gen_signat = generate_signature('api-sceret', verb, path, expires, data) the showing reponse<200> but returns empty list.
Looks like you're basing on this Bitmex sample. Please note that data must be JSON without whitespace between keys. The default separators value for json.dumps is (', ', ': '), so you need to change your code to use (',', ':'). It should be gen_signat = generate_signature('api-sceret', verb, path, expires, json.dumps(data,separators=(',', ':'))).