使用Haskell签署临时s3上传URL

时间:2013-11-13 08:30:44

标签: haskell encryption amazon-web-services amazon-s3

我正在尝试将文件从网络表单直接上传到Amazon S3异步。为此,我必须验证客户端请求以在服务器上上传文件。

通过使用我的AWS Secret密钥对上传请求进行数字签名,我可以创建一个临时验证的URL,客户端可以使用该URL将文件上传到S3存储桶。

亚马逊S3文档指定签名必须由以下

生成
Signature = URL-Encode( Base64( HMAC-SHA1( YourSecretAccessKeyID, 
                                         UTF-8-Encoding-Of( StringToSign ) ) ) );

我在服务器上使用Haskell,所以我的实现如下:

{-# LANGUAGE OverloadedStrings #-}
import qualified Data.ByteString.Base64.Lazy as B64
import qualified Data.Digest.Pure.SHA as SHA
import qualified Data.ByteString.Lazy.Char8 as BL8

sign :: BL8.ByteString -> BL8.ByteString
sign = B64.encode . SHA.bytestringDigest . SHA.hmacSha1 secret
  where secret = "aws-secret-key"

amazon docs的格式要求StringToSign看起来像:

StringToSign = HTTP-VERB + "\n" +
    Content-MD5 + "\n" +
    Content-Type + "\n" +
    Expires + "\n" +
    CanonicalizedAmzHeaders +
    CanonicalizedResource; 

亚马逊的另一个例子:

GET\n
\n
\n
1175139620\n

/johnsmith/photos/puppy.jpg

所以我的字符串看起来像:

  

“PUT \ n \ n \ n1384330538 \ N / bucketname / objname表”

我签名上面的字符串(带符号功能)并制作一个看起来像的网址:

https://s3.amazonaws.com/bucketname/objname?AWSAccessKeyId=accessskey&Signature=signature=&Expires=1384330979

然后在上传之前通过AJAX请求将其发送到客户端。我已经更新了存储桶上的CORS策略以允许PUT请求。

问题在于,每当我尝试使用上面签名的网址上传内容时,我都会收到此消息(在XML文档中)。

  

我们计算的请求签名与您的签名不符   提供。检查您的密钥和签名方法。

所以我不确定我哪里出错了。注意:如果我使用公共网址(https://s3.amazonaws.com/bucketname/objname),我可以上传(但这不应该,我只希望用户上传blob,不读取或删除等)。

2 个答案:

答案 0 :(得分:1)

作为一个经常做这种舞蹈的人,很难构建能正确签署HTTP-digest认证请求的软件。特别是,如果您仅依靠服务器响应来指导您,则需要很长时间。出于安全考虑,服务器在拒绝您时会故意隐晦。

我最好的建议是(a)获得一个你知道可行的替代实现,以及(b)将你的Haskell接口构建为纯粹的,这样就很容易让它完全复制来自其他框架的请求。(c)make确保您可以从备用框架和您自己的代码中同时获取完全请求文本和完全 String-To-Sign。特别是,您通常必须确定准确的时间戳和随机数,并密切关注百分比编码。

使用这两个工具,只需从备用实现创建各种成功的请求,看看是否可以使用自己的框架复制完整的String-To-Sign和确切的请求文本。

我自己的错误通常包括不正确的编码,缺少引号,不包括所有正确的参数(或错误的参数),或者错误地使用hmac函数。

答案 1 :(得分:0)

这是我的上传网址代码,我可能已经错过了几个导入,因为我把它拉出了深处。

{-# LANGUAGE OverloadedStrings, FlexibleContexts, TypeFamilies, DeriveDataTypeable, TemplateHaskell, QuasiQuotes #-}

import qualified Aws
import qualified Aws.Core          as Aws
import qualified Aws.S3            as S3
import qualified Data.Text         as T
import qualified Codec.Binary.Base64         as B64
import qualified Data.ByteString             as BS
import Text.Shakespeare.Text(st)
import qualified Codec.Binary.Url            as Url
import System.Posix.Time(epochTime)
import Crypto.MAC.HMAC(hmac)
import Crypto.Hash.SHA1(hash)

data Cfg = Cfg { baseCfg :: Aws.Configuration
               , s3Cfg :: S3.S3Configuration Aws.NormalQuery
               , s3Bucket :: S3.Bucket
               }

uploadUrl :: Cfg -> T.Text -> T.Text -> IO T.Text
uploadUrl cfg mime filename = do
   time <- epochTime
   let expires = show $ time + 600
       msg = E.encodeUtf8 $ [st|PUT

#{mime}
#{expires}
x-amz-acl:public-read
/#{s3Bucket cfg}/#{filename}|] --the gap is necessary
       key = Aws.secretAccessKey $ Aws.credentials $ baseCfg cfg
       accessid = T.pack $ Url.encode $ BS.unpack $ Aws.accessKeyID $ Aws.credentials $ baseCfg cfg
       signature = encode . T.pack $ B64.encode $ BS.unpack $ hmac hash 64 key msg
       encode = T.pack . Url.encode .  BS.unpack . E.encodeUtf8
   return $ [st|http://#{s3Bucket cfg}.s3.amazonaws.com/#{filename}?AWSAccessKeyId=#{accessid}&Expires=#{expires}&Signature=#{signature}|]