如何在Haskell中验证OpenSSH的Ed25519签名?
在Haskell中验证Ed25519签名失败问题
问题背景
我尝试在Haskell中验证Ed25519签名,通过ssh-keygen工具可以正常验证,但用crypton库实现时结果始终为false。操作步骤如下:
- 生成签名:
$ ssh-keygen -Y sign -f data/testkey_ed25519 -n file data/message.txt - 创建
allowed_signers文件:# allowed_signers testprincipal ssh-ed25519 <公钥> <注释> - 验证签名(成功):
$ ssh-keygen -Y verify -n file -s data/message.txt.sig -f data/authorized_signers -I testprincipal < data/message.txt Good "file" signature for testprincipal with ED25519 key SHA256:RmoAVYLzWd7b2pTB0O1ovGu/KhXosg0zk++pJgIvQjw
我的Haskell代码如下:
{-# LANGUAGE OverloadedStrings #-} module Main where import qualified Data.ByteString as BS import qualified Data.ByteString.Char8 as C import qualified Extra import qualified Crypto.PubKey.Ed25519 as Ed25519 import Crypto.Error import qualified Data.ByteString.Base64 as Base64 unarmorSignature :: C.ByteString -> C.ByteString unarmorSignature = removeHeader . removeFooter . removeLinebreaks where header = "-----BEGIN SSH SIGNATURE-----" footer = "-----END SSH SIGNATURE-----" removeHeader = C.drop (C.length header) . snd . C.breakSubstring header removeFooter = fst . C.breakSubstring footer removeLinebreaks = C.filter (/= '\n') main :: IO () main = do message <- BS.readFile "data/message.txt" print message signatureWrapped <- BS.readFile "data/message.txt.sig" let signatureDecoded = Extra.fromRight' $ Base64.decode $ unarmorSignature signatureWrapped let signatureBS = BS.takeEnd 64 signatureDecoded signature <- throwCryptoErrorIO $ Ed25519.signature signatureBS print signature keyfileContent <- BS.readFile "data/authorized_signers" let _:_:key:_ = C.words keyfileContent let keyDecoded = Extra.fromRight' $ Base64.decode key let Just keyBS = C.stripPrefix "\NUL\NUL\NUL\vssh-ed25519\NUL\NUL\NUL " keyDecoded key <- throwCryptoErrorIO $ Ed25519.publicKey keyBS print key let result = Ed25519.verify key message signature print result
代码意图是按RFC8709规范解析文件,提取Ed25519密钥与签名并验证。
问题原因
你忽略了SSH签名的上下文结构:Ed25519签名并非仅对原始消息生成,而是对包含SSH前缀、签名类型、命名空间(即-n file指定的file)和原始消息的组合数据签名。你的代码仅用原始消息验证,自然无法匹配。
另外,公钥解析逻辑有误:SSH公钥的结构是「4字节长度前缀 + ssh-ed25519字符串 + 4字节公钥长度前缀 + 32字节Ed25519公钥」,你手动拼接的前缀字符串不符合规范。
修复后的代码
{-# LANGUAGE OverloadedStrings #-} module Main where import qualified Data.ByteString as BS import qualified Data.ByteString.Char8 as C import qualified Extra import qualified Crypto.PubKey.Ed25519 as Ed25519 import Crypto.Error import qualified Data.ByteString.Base64 as Base64 import Data.Word (Word32) import Data.Binary.Get (runGet, getWord32be) import Data.Bits (shiftR) -- 解析SSH签名的元数据与签名值 parseSSHSignature :: BS.ByteString -> (BS.ByteString, BS.ByteString, BS.ByteString) parseSSHSignature bs = runGet parser bs where parser = do _ <- getWord32be prefix <- BS.getBytes 4 if prefix /= "SSH\x00" then error "无效的SSH签名前缀" else do sigTypeLen <- getWord32be sigType <- BS.getBytes (fromIntegral sigTypeLen) namespaceLen <- getWord32be namespace <- BS.getBytes (fromIntegral namespaceLen) return (sigType, namespace, BS.takeEnd 64 bs) -- 构造Ed25519验证所需的完整上下文数据 buildVerificationData :: BS.ByteString -> BS.ByteString -> BS.ByteString -> BS.ByteString buildVerificationData sigType namespace message = mconcat [ putWord32be (fromIntegral (BS.length "SSH\x00")) , "SSH\x00" , putWord32be (fromIntegral (BS.length sigType)) , sigType , putWord32be (fromIntegral (BS.length namespace)) , namespace , putWord32be (fromIntegral (BS.length message)) , message ] where putWord32be w = BS.pack [ fromIntegral (w `shiftR` 24) , fromIntegral (w `shiftR` 16) , fromIntegral (w `shiftR` 8) , fromIntegral w ] unarmorSignature :: C.ByteString -> C.ByteString unarmorSignature = removeHeader . removeFooter . removeLinebreaks where header = "-----BEGIN SSH SIGNATURE-----" footer = "-----END SSH SIGNATURE-----" removeHeader = C.drop (C.length header) . snd . C.breakSubstring header removeFooter = fst . C.breakSubstring footer removeLinebreaks = C.filter (/= '\n') main :: IO () main = do message <- BS.readFile "data/message.txt" signatureWrapped <- BS.readFile "data/message.txt.sig" let signatureDecoded = Extra.fromRight' $ Base64.decode $ unarmorSignature signatureWrapped let (sigType, namespace, signatureBS) = parseSSHSignature signatureDecoded signature <- throwCryptoErrorIO $ Ed25519.signature signatureBS keyfileContent <- BS.readFile "data/authorized_signers" let _:_:keyStr:_ = C.words keyfileContent let keyDecoded = Extra.fromRight' $ Base64.decode keyStr -- 正确提取Ed25519公钥:跳过前4+11字节(长度前缀+ssh-ed25519),取32字节 let keyBS = BS.take 32 $ BS.drop (4 + 11) keyDecoded key <- throwCryptoErrorIO $ Ed25519.publicKey keyBS -- 用完整上下文数据验证签名 let verificationData = buildVerificationData sigType namespace message let result = Ed25519.verify key verificationData signature print result
关键修复点
- 构造完整验证上下文:按照RFC8709要求,拼接SSH固定前缀、签名类型、命名空间和原始消息,生成签名时的完整数据体。
- 修正公钥解析:直接跳过SSH公钥的元数据前缀,提取标准32字节Ed25519公钥。
- 解析签名元数据:从签名Blob中提取签名类型和命名空间,确保验证上下文与签名时完全一致。
内容的提问来源于stack exchange,提问作者bDcb
相关产品推荐
相关产品推荐

