算法交易时如何存储 API 密钥?

信息安全 验证 密码 api
2021-08-25 22:59:21

我为客户编写 Python 脚本以在货币/证券交易所网站上进行“算法交易”。我的客户通常在传统的个人台式电脑上运行我的脚本。通常也将这些 PC 用于网络浏览活动。环境始终是 Linux;通常是 Debian。在行业内;以这种方式进行算法交易,Python 是相当标准的;无论是体制上还是私下。

但是,我不禁看到了安全模型中的一个缺陷。

每个交易所的认证方式略有不同,但总之有:

USER INPUTS:

api['secret']       # private key from exchange

USER CONFIG FILE CONTAINS:

api['key']          # public key from exchange
api['exchange']     # name of exchange; ie "binance"
api['symbol']       # market pair symbol in format BTC:USD
api['uri']          # the url up to .com/

FROM USER INPUTS SCRIPT BUILDS REQUEST SPECIFIC:

api['nonce']        # time.time() at beginning of request
api['endpoint']     # path/to/server/resource
api['url']          # uri + endpoint
api['method']       # GET, POST, or DELETE
api['params']       # dict with request specific parameters
api['data']         # str with request specific parameters
api['headers']      # authentication signature specific to the request

这些请求是这样的:

POST BUY/SELL
DELETE BUY/SELL (CANCEL)
GET ACCOUNT BALANCES
GET OPEN ORDERS
WITHDRAW FUNDS

以下是我写给一些货币交易所的认证方法的一些例子;它们在外汇/股票/加密交易行业都是相当标准的。主要是通用格式:

api["header"] = {"signature": HMAC(SHA256(the_request_parameters))}

例子:

def signed_request(api, signal):
    """
    Remote procedure call for authenticated exchange operations
    api         : dict with keys for building external request
    signal      : multiprocessing completion relay
    """
    api = lookup_url(api)
    api["data"] = ""
    if api["exchange"] == "coinbase":
        api["data"] = json_dumps(api["params"]) if api["params"] else ""
        api["params"] = None
        message = (
            str(api["nonce"]) + api["method"] + api["endpoint"] + api["data"]
        ).encode("ascii")
        secret = b64decode(api["secret"])
        signature = hmac.new(secret, message, hashlib.sha256).digest()
        signature = b64encode(signature).decode("utf-8")
        api["headers"] = {
            "Content-Type": "Application/JSON",
            "CB-ACCESS-SIGN": signature,
            "CB-ACCESS-TIMESTAMP": str(api["nonce"]),
            "CB-ACCESS-KEY": api["key"],
            "CB-ACCESS-PASSPHRASE": api["passphrase"],
        }
    elif api["exchange"] == "poloniex":
        api["params"]["nonce"] = int(api["nonce"] * 1000)
        message = urlencode(api["params"]).encode("utf-8")
        secret = api["secret"].encode("utf-8")
        signature = hmac.new(secret, message, hashlib.sha512).hexdigest()
        api["headers"] = {
            "Content-Type": "application/x-www-form-urlencoded",
            "Key": api["key"],
            "Sign": signature,
        }
    elif api["exchange"] == "binance":
        api["params"]["timestamp"] = int(api["nonce"] * 1000)
        api["params"]["signature"] = signature
        message = urlencode(api["params"].items()).encode("utf-8")
        secret = bytes(api["secret"].encode("utf-8"))
        signature = hmac.new(secret, message, hashlib.sha256).hexdigest()
        api["headers"] = {"X-MBX-APIKEY": api["key"]}
    elif api["exchange"] == "bittrex":
        api["params"]["apikey"] = api["key"]
        api["params"]["nonce"] = int(api["nonce"] * 1000)
        message = api["url"] + api["endpoint"] + urlencode(api["params"])
        message = bytearray(message, "ascii")
        secret = bytearray(api["secret"], "ascii")
        signature = hmac.new(secret, message, hashlib.sha512).hexdigest()
        api["headers"] = {}
    elif api["exchange"] == "kraken":
        api["data"] = api["params"][:]
        api["params"] = {}
        data["nonce"] = int(1000 * api["nonce"])
        api["endpoint"] = "/2.1.0/private/" + api["endpoint"]
        message = (str(data["nonce"]) + urlencode(data)).encode("ascii")
        message = api["endpoint"].encode("ascii") + hashlib.sha256(message).digest()
        secret = b64decode(api["secret"])
        signature = b64encode(hmac.new(secret, message, hashlib.sha512).digest())
        api["headers"] = {
            "User-Agent": "krakenex/2.1.0",
            "API-Key": api["key"],
            "API-Sign": signature,
        }
    elif api["exchange"] == "bitfinex":
        nonce = str(int(api["nonce"] * 1000))
        api["endpoint"] = path = "v2/auth/r/orders"
        api["data"] = json.dumps(api["params"])
        api["params"] = {}
        message = ("/api/" + api["endpoint"] + nonce + api["data"]).encode("utf8")
        secret = api["secret"].encode("utf8")
        signature = hmac.new(secret, message, hashlib.sha384).hexdigest()
        api["headers"] = {
            "bfx-nonce": nonce,
            "bfx-apikey": api["key"],
            "bfx-signature": signature,
            "content-type": "application/json",
        }

    url = api["url"] + api["endpoint"]
    ret = requests.request(
        method=api["method"],
        url=url,
        data=api["data"],
        params=api["params"],
        headers=api["headers"],
    )
    response = ret.json()

我的客户经常问我,在哪里存储api["secret"]. 在配置文件中?在环境变量中?每次重新启动时手动输入并将其物理存储在纸上?我没有好的答案和任何建议……我赶紧捂脸。

我开始编写一个应用程序 - 在 python 中 - 来存储 API 密钥:

主要特点:

  • 给定URLkey输入:

  • 使用secretxclip写入剪贴板

  • 在 10 秒内自动清除剪贴板

安全特性:

  • 读取/写入 AES CBC 加密密码 JSON 到文本文件

  • salt 是 16 字节的shake256,以加密安全方式生成,带有 os.urandom

  • 每次返回主菜单并退出后添加新盐以防止字典攻击

  • 主密码延长至 400 兆字节以防止 GPU/FPGA 攻击

  • 主密码通过传统的盐渍 pbkdf sha512 迭代 1,000,000 次以防止暴力攻击

  • 只有第 3 方模块是“pycryptodome”;如果发现已弃用的“pycrypto”,则引发异常

  • 编辑脚本需要 sudo 系统密码

我的想法是我的用户可以使用这个应用程序来保护他们的密钥,并且在脚本加密意义上......我已经点缀了我的 I's 并越过了我的 t's......当它们被存储时,它们被存储了。时期。我很相信你不能破解我的加密方案。

你可以在这里或通过谷歌搜索找到我的应用程序:litepresence/CypherVault

但我还是捂脸。我在这里打开了一个Reddit/r/security帖子来讨论我的应用程序,我的 facepalm 很快就得到了验证。

归根结底……无论我如何处理 api["secret"],它最终都会在 RAM 上被恶意软件转储并上传给攻击者。

当用户输入secret要加密的...它在 RAM 上。当脚本解密secret以签署交易时......它在RAM上。

然后恶作剧……“我的脚本”导致某人赔钱,因为它不“安全”;我有责任感。

如何避免这种情况?一个人如何安全地存储交换 API 机密以组成金融交易 - 由机器人 - 在脚本语言中,在台式机上,而不在某些时候将它们暴露给你的RAM,甚至可能更糟......你的SWAP

2个回答

无论我如何处理 api["secret"],它最终都会在 RAM 上被恶意软件转储并上传给攻击者。

...

如何避免这种情况?

不可能。在基于计算机的身份验证步骤中使用密码的要求与不希望计算机访问密码之间存在根本矛盾。没有第三种方法。

您可以在不使用时对其进行加密(听起来您已经这样做了)。您可以在不使用时将其覆盖在内存中。但是当计算机需要使用它时,它会在内存中。

根据您的威胁模型,这可能没什么大不了的。 政府级别的物理攻击是内存内容最常见的问题,但很少有人需要对此进行防御。

这正是硬件安全模块 (HSM) 旨在解决的问题。HSM 有自己的嵌入式处理器,并包含一个或多个私钥,这些私钥永远不会离开 HSM。因此,在 HSM 所连接的机器上运行的程序无法访问 HSM 上的私钥。

HSM 可用于向服务器进行身份验证,通过证明它拥有与用户的公钥对应的私钥。HSM 通过使用私钥及其板载处理器(例如数字签名或共享密钥)计算值来实现此目的。数字签名或共享秘密基于服务器提供的临时值,因此能够拦截这些数字签名/共享秘密的攻击者不能重用数字签名/共享秘密(即,该解决方案可以抵抗重放攻击)。

当然,您使用的服务必须支持这种类型的身份验证。