1个不稳定版本

0.1.0 2024年6月6日

#1184 in 密码学

MIT/Apache

75KB
1.5K SLoC

sshauth:一个基于SSH密钥的身份验证令牌库

sshauth库可以生成并验证由SSH私钥签署的临时携带令牌,无论是直接还是通过SSH代理。这些令牌适用于客户端对服务器的身份验证;例如,使用HTTP Authorization头。

示例

在客户端,选择一个SSH密钥(在SSH术语中称为“身份”),无论是通过读取密钥文件还是查询代理(有关密钥管理的更多信息,请参阅ssh-agent(1)ssh-add(1)

let authsock = env::var("SSH_AUTH_SOCK")
    .expect("SSH_AUTH_SOCK is unset or invalid");
let public_key: ssh_key::public::PublicKey =
    sshauth::agent::list_keys(&authsock)
    .await?
    .into_iter()
    .find(|key| {
        /*
         * Choose key of interest by type, fingerprint, comment, etc.
         */
        ...
    })
    .expect("can't find a suitable SSH identity");

拥有密钥后,客户端可以生成、签名并编码令牌

let signer = sshauth::TokenSigner::using_authsock(authsock)?
    .key(key)
    .include_fingerprint(true)
    .build()?;
let token: String = signer
    .sign_for()
    .sign()
    .await?
    .encode();

然后,假设公钥已在服务器上预先注册并存储(例如,在数据库或文件系统中),服务器中的身份验证代码可以查看未经验证的令牌中的指纹,检索相应的公钥,并验证令牌的签名

/*
 * Get the token; e.g., from the "Authorization" header of a HTTP request:
 */
let raw_token: String = "...";
let unverified_token = sshauth::UnverifiedToken::try_from(raw_token.as_str())?;
let fingerprint = unverified_token
    .untrusted_fingerprint()
    .expect("token must include fingerprint");
/*
 * Fetch the registered public key matching the fingerprint and verify the
 * signature:
 */
let public_key = your_database.key_for_fingerprint(&fingerprint)?;
let verified_token = unverified_token.verify_for().with_key(&public_key)?;

时钟要求

为了限制令牌的有效寿命,当前日期和时间被包含在签署的blob中,以便可以在服务器上验证。默认情况下,客户端和服务器时钟必须在60秒的时间差内达成一致。建议客户端和服务器都配置NTP以避免问题。

如果您的系统提供更宽松的时间保证,或者您想能够更长时间地重用令牌,则可以放宽此要求 - 这将相应地降低面对泄露或截获的令牌时的安全性。在验证令牌时使用.max_skew_seconds(seconds),范围更广。目前无法完全禁用时间戳生成或验证。

请注意,虽然客户端正在签名时间戳,但这并不意味着客户端在指定的时间产生了签名。客户端时钟受客户端控制,在产生签名时可以任意调整或倒退。如果您需要一个可验证的时间戳,则需要某种其他机制来实现这一点;例如,一组独立的时间戳签名服务器,这些服务器可以使用SSH令牌进行身份验证。

操作

为了减少重放攻击的范围,签名块可以包含一个任意动作列表,描述为字符串键值对。应用程序应将希望验证的任何内容包含在动作列表中,例如附加令牌的请求的HTTP方法、授权、查询字符串和POST主体的摘要,这将使攻击者更难重新使用截获的令牌来发出不同的请求。

为了保持令牌大小可管理,动作列表不包括在令牌本身中。令牌签名者和验证者必须事先同意动作列表的顺序和精确内容。任何不匹配都将导致验证失败。

魔法前缀

为了使跨协议攻击更加困难,库支持使用.magic_prefix([u8; 8])函数在签名者和验证者中进行签名。使用不同魔法前缀的两个系统将生成彼此无法验证的签名。客户端和服务器必须在它们相互同意的应用协议级别上就一个常数前缀值达成一致。

身份

作为使用指纹来识别密钥的替代方案,库也支持使用(受限的)文件名;如果您使用文件系统作为密钥存储或想要通过合成标识符(如登录名)查找密钥时,这可能会很有用。只需在客户端将.include_fingerprint(true)替换为.identity_filename(path),在服务器端而不是使用.untrusted_fingerprint(),而是使用.untrusted_identity_filename()

如果需要,还可以在同一令牌中将身份文件名和包含的指纹结合起来使用。

公钥来源

库为小型应用程序提供两个基本助手模块,这些应用程序主要在本地文件系统上运行,以开始使用公钥来源

  • keyfile::parse_authorized_keys()函数将解析OpenSSH的authorized_keys文件,并从该文件返回一个公钥列表。
  • keydir::KeyDirectory对象在此基础上执行在指定目录中的查找,使用令牌中可能提供的身份文件名。该文件名将用于从密钥目录中加载文件,如果该文件中的任何公钥允许成功验证,则令牌被认为是有效的。

系统目前支持以下密钥类型

  • 使用NIST P-256曲线的ECDSA密钥
  • Ed25519密钥

可调整

有许多其他可调整的令牌参数(过期时间、签名策略等),但对于许多应用程序而言,默认值应该是合理的。

库旨在难以误用。如果您发现一种“错误使用”的方式(例如,在验证之前信任数据、构建不安全或未签名的令牌、跳过或不当验证签名等),请告诉我们

限制

这个库的适用范围仅限于公钥认证。它并不打算成为一个通用的身份提供者或授权系统。没有网络组件或与任何第三方服务的集成。它不直接支持任何类型的挑战/响应协议(尽管可以通过使用操作键值对在顶层进行分层),因此原则上容易受到某些类型的重放攻击。如果您需要这些功能中的任何一项,可能其他协议如SAML、OAuth、OIDC等更适合。

如果这个库能够验证令牌,您可以假设该令牌的制作者在生成时有权访问相应的私钥,这在允许的窗口内。在没有其他信息的情况下,您通常不能对签名系统、用户或私钥的所有者做任何假设。

贡献

欢迎任何形式的帮助性贡献;请在我们的问题拉取请求上提交,请访问我们的GitHub仓库

依赖项

约8-17MB
约237K SLoC