宾诺斯
这个 @exodus/binauth 软件包提供了一个无国籍认证机制,允许客户端将自己认证到服务器上,作为给定的ed25519公钥. 验证符与挑战通过签名二进制数据来发布.
议定书介绍如下。 包含代码示例,说明代码是运行在服务器还是客户端上. 客户端和服务器之间的数据传输可以以任何几种方式进行:HTTPS请求,websocket消息等. 这是留给此库用户的练习.
- 服务器生成其密钥对,并设置一个
binauth服务对象从密钥对。 服务器的 keypair 应当保持一致, 这样提供给客户端的认证符仍然有效 。 这可以通过保存用于生成键的 en来完成,也可以通过保存键本身并从环境变量中加载来完成. 使用a.env文件 这是在不暴露您源代码中的私密密钥数据的情况下实现此目的的好方法.
// Server
const ed25519 = require('@exodus/crypto/ed25519')
const createBinauth = require('@exodus/binauth')
// entropy should be generated randomly and not exposed publicly.
// doing it like this for the sake of demonstration
const privateKey = Buffer.from(
'551a4b322d59e692c7007d8e296ca95b01c22a82f6a428504852ffc7e60675ac',
'hex'
)
// either the entropy, or the publicKey and privateKey should be saved
const publicKey = await ed25519.edwardsToPublic({ privateKey })
const binauth = createBinauth({
serverPublicKey: publicKey,
serverPrivateKey: privateKey,
})
- 客户端生成其密钥对. 这是一个基于公钥的认证系统,因此如果客户端想要保持一个一致的身份,他们也必须保持一个一致的密钥对.
// Client
const ed25519 = require('@exodus/crypto/ed25519')
// entropy should be generated randomly and not exposed publicly.
// doing it like this for the sake of demonstration
const privateKey = Buffer.from(
'995007b62f7b2519b1ff34337470db9e323e32ec7118fbe283559add6891df3f',
'hex'
)
// either the entropy, or the publicKey and privateKey should be saved.
const publicKey = await ed25519.edwardsToPublic({ privateKey })
-
客户端向服务器发送其公钥.
-
服务器对客户端的公用钥匙发出挑战. 一个挑战是一个字节数组,由一个版本的字节来说明它是一个挑战,客户端的公钥,以及一个时间戳. 挑战随后由服务器的私人密钥签名,这样服务器以后可以验证挑战是否真实,从而允许无国籍. 所有这一切都被封装在内
binauth.getChallenge,返回已签名的挑战.
// Server
const challenge = await binauth.getChallenge(publicKey)
console.log(challenge.toString('base64')) // 'AcwBvnPeBHTRw...'
-
服务器将挑战还给客户端.
-
客户端在服务器发布的质疑上签名. 此签名为服务器提供证据,证明客户端拥有对应客户端先发公钥的私钥.
// Client
const signedChallenge = await ed25519.signAttached({ message: challenge, privateKey })
-
客户端向服务器发送其公钥和已签名的挑战.
-
服务器验证已签名的挑战 。 由此,出现了一船一船的验证:
- 服务器验证签名的挑战由客户端给出的公钥签名.
- 在解开客户端签名后,服务器自己的签名也会被验证.
- 初始挑战字节数组的公钥与客户端给出的公钥进行比较——它们必须是等同的.
- 服务器验证初始挑战的版本字节正确.
- 服务器验证初始挑战的时间戳在目前时间的已定义的挑战时间到活(TTL)范围内,并且不是在未来.
如果所有验证步骤都成功,服务器会签名并发布认证令牌,这将授权该客户端控制给定的公钥. 已签名的认证令牌与已签名的挑战格式相同 binauth.getChallenge,带有不同的版本字节,客户端的公钥,以及当前的时间戳. 以上所有内容都包含在方法内 binauth.getToken。 。 。 .
如果校验失败, 就会出现错误, 可能有一个 .statusCode 属性. .statusCode 如果计划中的验证步骤不成功,则提供,并且状态代码说明发生故障的类别。 现在,可以是 400 (恶意请求), 401 (未经授权),或 undefined如果 .statusCode 实值 undefined,它应该被认为是一个内部服务器错误,并且可能是应该报告的错误.
// Server
try {
const authToken = await binauth.getToken(publicKey, signedChallenge)
console.log(authToken.toString('base64')) // 'UntZvh3hKSPtY3...'
} catch (err) {
// BadRequest - client is trying to manipulate the server, forge signatures, etc
if (err.statusCode === 400) throw err
// Unauthorized - the challenge is not signed properly, or is expired.
// Maybe the server changed key pairs.
if (err.statusCode === 401) throw err
// InternalServerError - something unexpected went wrong
throw err
}
-
服务器返回认证符给客户端. 客户端现在被"认证",可以作为HTTP请求头,URL查询参数,在请求正文,websocket消息等向服务器传递认证令牌.
-
要验证认证符, 服务器会调用
binauth.verifyToken(authToken)。这证明:
- 签名来自服务器的私人密钥.
- 符号的版本字节是认证符号.
- 该符号的时间戳位于当前时间的已定义认证符号(TTL)内,并且不在未来.
如果上述验证步骤成功, 此方法返回 32 字节的公钥 Buffer,包含在认证符中。 这是被认证客户身份的直接代表.
如果校验失败, 就会出现错误, 可能有一个 .statusCode 属性. .statusCode 如果计划中的验证步骤不成功,则提供,并且状态代码说明发生故障的类别。 现在,它只能是 401 (未经授权)或 undefined如果 .statusCode 实值 undefined,它应该被认为是一个内部服务器错误,并且可能是应该报告的错误.
// Server
try {
const publicKey = await binauth.verifyToken(authToken)
} catch (err) {
// Unauthorized - the token is not signed properly, or is expired.
// Maybe the server changed key pairs.
if (err.statusCode === 401) throw err
// InternalServerError - something unexpected went wrong
throw err
}
安全考虑
服务器和客户端之间的数据传输只能通过安全通道进行. 该图书馆并不试图提供MitM保护. 委托给运输层(即TLS).
如果您计划在多个服务器上使用此认证协议, 那么每个服务器都会与普通客户端交谈 您应当确保客户端和服务器密钥对不被再利用 在不同服务器客户端上下文之间.
重用 服务器 密钥对将意味着对一个服务器有效的认证符号和挑战对另一个服务器也是有效的,如果一个服务器有弱点,那么它们都一样。 应避免这种情况,因为它没有带来任何好处.
重用 客户端 密钥对可能导致升级攻击,通过对一服务器的控制,攻击者可以冒充其他服务器上的普通客户端.
比如说 服务器 A 财务报告和审定财务报表 服务器 B 两者都和同一个客户端交谈,两个服务器都使用相同的公钥对. 让我们想象一下服务器A被攻击者接管. 用Server A认证的客户端会发送他们的公用密钥以获得挑战. Server A可以使用该公钥从Server B那里取取出一个挑战,并将这个挑战交给客户端. 客户端会盲目地签名,并将签名的挑战发送到Server A. 控制服务器A的攻击者现在可以使用所签名的挑战来生成认证符并从而冒充服务器B上的客户端.
最自然的解决方案是为每一个新的服务器-客户端上下文创建新的密钥对,在两侧. 这保证了如果一个上下文的认证符或密钥被曝光,或者即使一个服务器被接管,也不能影响其他的认证上下文.
服务器标识
然而,如果你不能保证钥匙配对不会被再使用,则有另一种安全机制. 你可以通过 serverId 页:1 createBinauth 函数。 此选项 如果您计划跨多个服务器重新使用客户端密钥对,则应当使用. 每个服务器必须设置一个独特而一致的 serverId 初始化时 binauth。 。 。 .
客户端与服务器通话 应 预留适当的 serverId 在签署前遇到挑战 这使得签名的挑战无法在不同的服务器上有效. 以下是一个试验案例的例子:
// This is a challenge issued by Server B's keypair from the example attack vector above.
const binauth = createBinauth({ serverId: 'Server B', serverPublicKey, serverPrivateKey })
const challenge = await binauth.getChallenge(publicKey)
// This imaginary client believes it is talking to Server A, so it signs the challenge
// using 'Server A' as the server ID. This prevents a takeover of Server A from
// impacting client keypairs which are reused on Server B.
const signedChallenge = await ed25519.signAttached({
privateKey,
message: Buffer.concat([Buffer.from('Server A'), challenge]),
})
即使服务器指定了 serverId,客户端也可以选择服务器不可知性,只需将挑战签为-is. 即使没有预设,服务器仍然会接受签名的挑战 serverId在这种情况下,客户的公用钥匙身份容易受到上述升级攻击.
财务问题
你一直提到这个制度 无国籍者. 这是什么意思?
A 类 状态 认证系统将涉及在状态数据存储机制(数据库、内存、磁盘文件等)中存储令牌或登录类型事件。 一个具有状态的认证系统的例子就是用任何手段验证身份,但随后必须将该验证事件存储在它的存储中,这样它不会忘记它——比如写下一个说明‘token ABC由用户X持有'. 当用户X稍后带着代号ABC回来,试图认证时,它会在其存储中查取给定的无记名代号"ABC". 它会检查代币的过期, 最后确认 我们应该给持证人 与用户X的身份.
与此相反,a 无国籍人 认证系统不需要存储 。 相反,我们有一套纯粹的功能,它们各自保证它们接收和返回的数据,即等待客户端输入的每一个步骤。 为了实现无国籍状态,所有必要的数据都包含在这些函数的输入和输出值中. 一旦我们到达尽头 verifyToken 程序已经证明客户在经过核实的时间内控制了一对特定的公用钥匙,并且应当将其视为控制可配置的时间(tokenTTL) (中文(简体) ).
无国籍状态为何有用?
需要无国籍状态的常见情况:
- 无服务器架构(如AWS Lambda),其中文件系统与持续内存无法被依赖.
- 分布式系统,就单一的真相来源达成协议是一项挑战.
- 磁盘空间和内存有限的土豆系统可供存储.
无国籍认证对缩放有用. 如果您的服务器需要验证大量客户端, a 状态 认证系统可能会给您的服务器的存储机制造成严重压力. 无国籍人 另一方面,认证将随着处理能力向上扩展,并且可以在没有任何种族条件的情况下进行整齐平行.
无国籍认证也可能有助于简化您的项目 。 例如,您可以选择无国籍身份认证,以便释放您的服务器对数据库的需求.
无国籍状态如何不足?
有些认证协议在无国籍认证的情况下是不可能的. 由于没有州,没有可以有选择地撤销的"会议". Singleton认证是不可能的,因为客户端必然能够创建无限数量的认证符(前提是他们能够证明对一对公钥的控制).
一旦发出质疑或认证令牌,即有效,直至失效,或 直到服务器的密钥对被更改.
为什么不签每个请求? 或客户端产生自己的"挑战"并签名,以节省请求数量?
我们让客户端在服务器发布的质疑上签名,这样客户端就可以对服务器端认证的操作进行不公开. 客户端只需要获取一个挑战,签名,并交给服务器,以换取一个无记名认证符. 客户不需要知道什么是挑战 这是, 也没有任何关于服务器如何认证上面的签名。 它不需要决定要签什么数据,如何将数据序列化等等. 对客户端的时钟错误并不担心,因为服务器在信使的信息正文中提供并标出时间戳. 简言之,它为服务器一侧留下了更大的灵活性空间.
如何处理一个已损坏的服务器密钥 ?
如果一个服务器的私人密钥(或用来生成一对密钥的 en)被损坏,则必须在任何使用同一对密钥的服务器实例上修改ASAP.
在更改了服务器密钥对后,所有之前发布的质疑和认证令牌都会在新密钥对下无效. 这是因为上面的签名来自旧的已损坏的一对密钥,在用新一对密钥进行签名验证时会失败. 当客户端以错误的签名提交质疑和令牌时,服务器必须处理,并向客户端表明他们必须重新认证.
换句话说,服务器应该返回 a 401 当检测到服务器本身的签名不正确时。 当客户得到一个 401,它应该重新运行认证程序,以确保它从新的服务器密钥对得到一个挑战与认证令牌.
如何处理损坏的客户端密钥?
一旦被泄露,私钥就不能不妥协. 这意味着公用钥匙身份不再安全,也不可能重新获得安全. 客户端必须旋转其密钥并认证,仿佛它是一个全新的客户端.
因此,客户应尽心尽力,保持其私人钥匙的安全,与经认证后可获得的特权相称。 您还不妨考虑在客户认为私钥已失密时, 实施“ 自毁” 功能 。 自毁相当于删除账户:清理经认证的客户可能获取的任何敏感数据,并撤销其曾经形成的任何能力.
如何处理损坏的认证标志?
被妥协的认证符与被损坏的客户端密钥相比,没有那么危险,因为它们已经过期. 期满后,它们不能再被使用,但与此同时,无法有选择地取消它们.
如果一个客户端的认证令牌被不知何故的泄露而他们的私人密钥被泄露,那么撤销的唯一方法就是通过旋转服务器密钥对来去认证所有用户. 这样就无效了 全部 标志和挑战,到处,同时。 有效的客户端应当通过相应的客户端实施,实现无缝复出.
或者,在理论上可以先强制规定指使符的区块列表 getToken 编号 : verifyToken 但建议不予采纳,因为维持这种封面清单可能具有挑战性.
如果您的实施对认证标志具有暴露风险,建议设定一个简短的 tokenTTL 这样,暴露的标志 携带的风险最小.
API 参考
createBinauth
主导出 @exodus/binauth。 。 。 .
此函数创建了用于根据给定的公/私密密钥对创建并验证挑战与认证符的方法.
参数:
options数字 :Object(必须)serverPublicKey数字 :Buffer(需要) - 服务器的ED25519公钥.serverPrivateKey数字 :Buffer(需要) - 服务器的ED25519私人密钥.serverId数字 :String(可选) - 识别服务器的字符串 。 见 服务器标识指令。 。 。 .challengeTTL数字 :Number(可选) - 新发布的挑战前的毫秒数将过期. 默认1小时。 如果通过,必须是正整数.tokenTTL数字 :Number(可选) - 在新铸造的认证符前的毫秒数将过期 。 默认为一日. 如果通过,必须是正整数.
回返情况:
binauth数字 :ObjectgetChallenge数字 :FunctiongetToken数字 :FunctionverifyToken数字 :Function
getChallenge
通过签署包含给定公用钥匙和时间戳的电文,向给定公用钥匙发出挑战。 客户会有的 challengeTTL 签名生成的挑战数据毫秒 Buffer,然后将签名的挑战发送到服务器供用户使用 getToken。 。 。 .
参数数字 :
publicKey数字 :Buffer(需要) - 32字节的ED25519公钥,将发布挑战.
回返数字 :
Promise<Buffer>- 必须由客户私人钥匙签名的挑战.
getToken
测试一个签名的挑战。 如果签名的挑战正确证明了对密钥对的控制,则会发出认证符.
参数数字 :
publicKey数字 :Buffer(需要) - 挑战被发布的32字节ED25519公钥.signedChallenge数字 :Buffer(要求) - 来自binauth.getChallenge,在与客户端的私钥签名后.
回返数字 :
Promise<Buffer>- 使持有人能够通过下列途径认证的认证标志:binauth.verifyToken,作为给定publicKey直到它过期,或者直到服务器的密钥对被更改.
verifyToken
测试认证符号。 如果有效,则返回指使物持有人已证明控制权的公用钥匙.
参数数字 :
token数字 :Buffer(需要) - 认证符binauth.getToken。 。 。 .
回返数字 :
Promise<Buffer>- 认证证书持有人控制的公用钥匙.
测试矢量
对于希望以其他语言或领域重新执行这项协议的人,请检查 测试向量 文档,以及 看看单位如何对照他们检查执行情况。 。 。 .