You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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

关键修复点

  1. 构造完整验证上下文:按照RFC8709要求,拼接SSH固定前缀、签名类型、命名空间和原始消息,生成签名时的完整数据体。
  2. 修正公钥解析:直接跳过SSH公钥的元数据前缀,提取标准32字节Ed25519公钥。
  3. 解析签名元数据:从签名Blob中提取签名类型和命名空间,确保验证上下文与签名时完全一致。

内容的提问来源于stack exchange,提问作者bDcb

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.04 18:24:55