cloudfront-signer (empty) → 0.0.0.1
raw patch · 8 files changed
+262/−0 lines, 8 filesdep +RSAdep +asn1-encodingdep +asn1-typessetup-changed
Dependencies added: RSA, asn1-encoding, asn1-types, base, base64-bytestring, bytestring, crypto-pubkey-types, old-locale, time
Files
- .gitignore +10/−0
- .hub +1/−0
- .project +11/−0
- LICENSE +27/−0
- README.md +5/−0
- Setup.hs +2/−0
- cloudfront-signer.cabal +39/−0
- src/Aws/CloudFront/Signer.hs +167/−0
+ .gitignore view
@@ -0,0 +1,10 @@+dist+cabal-dev+*.o+*.hi+*.chi+*.chs.h+.virthualenv+.project+*~+.hub
+ .hub view
@@ -0,0 +1,1 @@+atlas
+ .project view
@@ -0,0 +1,11 @@+<?xml version="1.0" encoding="UTF-8"?>+<projectDescription>+ <name>cloudfront-signer</name>+ <comment></comment>+ <projects>+ </projects>+ <buildSpec>+ </buildSpec>+ <natures>+ </natures>+</projectDescription>
+ LICENSE view
@@ -0,0 +1,27 @@+Copyright (c) 2013, Lainepress+All rights reserved.++Redistribution and use in source and binary forms, with or without modification,+are permitted provided that the following conditions are met:++ Redistributions of source code must retain the above copyright notice, this+ list of conditions and the following disclaimer.++ Redistributions in binary form must reproduce the above copyright notice, this+ list of conditions and the following disclaimer in the documentation and/or+ other materials provided with the distribution.++ Neither the name of the {organization} nor the names of its+ contributors may be used to endorse or promote products derived from+ this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,5 @@+cloudfront-signer+=================++Provides functions for reading in the signing keys from file and making up+time-limited, signed URLS for accessing CloudFront-hosted files.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ cloudfront-signer.cabal view
@@ -0,0 +1,39 @@+Name: cloudfront-signer+Version: 0.0.0.1+Synopsis: CloudFront URL signer+Description: Provides functions for reading in the signing keys from file and making up time-limited, signed URLS for accessing CloudFront-hosted files.+Homepage: http://github.com/cdornan/cloudfront-signer+Author: Chris Dornan+Maintainer: chris@chrisdornan.com+License: BSD3+License-file: LICENSE+Copyright: (C) Chris Dornan+Category: Network+Build-type: Simple++Cabal-version: >= 1.14++Source-repository head+ type: git+ location: https://github.com/lainepress/cloudfront-signer++library+ Hs-Source-Dirs: src++ Build-depends: + RSA >= 1.2.2.0 ,+ asn1-types >= 0.2.0 ,+ asn1-encoding >= 0.8.0 ,+ base == 4.* ,+ base64-bytestring == 1.0.* ,+ bytestring >= 0.9 ,+ crypto-pubkey-types >= 0.4.0 ,+ old-locale >= 1 ,+ time >= 1.1.4++ Exposed-modules:+ Aws.CloudFront.Signer+ + Default-Language: Haskell2010++ GHC-Options: -Wall
+ src/Aws/CloudFront/Signer.hs view
@@ -0,0 +1,167 @@+{-# LANGUAGE RecordWildCards #-} ++module Aws.CloudFront.Signer+ ( URL+ , JSONPOlicy+ , CloudFrontSigningKey(..)+ , CloudFrontPolicy(..)+ , readCloudFrontSigningKeyFromDER+ , parseRSAPrivateKeyDER+ , signCannedPolicyURL+ , signCustomPolicyURL+ , signCustomPolicyURL_+ , cannedPolicy+ , customPolicy+ , unixTime+ ) where++import qualified Data.ASN1.Encoding as A+import qualified Data.ASN1.BinaryEncoding as A+import qualified Data.ASN1.Types as A+import qualified Data.ByteString.Lazy.Char8 as LBS+import qualified Data.ByteString.Base64.Lazy as B64+import Data.Time+import Data.Maybe+import Codec.Crypto.RSA+import qualified Crypto.Types.PubKey.RSA as C+import Text.Printf+import System.Locale+++type URL = String -- | input and output URLs+type JSONPOlicy = String -- | a JSON CloudFront policy+type KeyID = String -- | the CloudFront key pair identifier+++-- | a CloudFront siging key has an identifier and an RSA private key++data CloudFrontSigningKey+ = CloudFrontSigningKey+ { cfk_key_id :: KeyID+ , cfk_key :: PrivateKey+ }+ deriving (Show)++-- | a CloudFront policy must identify the resource being accessed and the+-- expiry time; a starting time and IPv4 address may also be specified++data CloudFrontPolicy+ = CloudFrontPolicy+ { cfp_Resource :: URL+ , cfp_DateLessThan :: UTCTime+ , cfp_DateGreaterThan :: Maybe UTCTime+ , cfp_IpAddress :: Maybe String+ }++-- | RSA private keys can only be read from DER file for now (the OpenSSL+-- tools can be used to convert from PEM:+--+-- openssl rsa -in input.pem -inform PEM -out output.der -outform DER+--++readCloudFrontSigningKeyFromDER :: KeyID -> FilePath -> IO CloudFrontSigningKey+readCloudFrontSigningKeyFromDER ki fp = + do pk_b <- LBS.readFile fp+ case parseRSAPrivateKeyDER pk_b of+ Left err -> error err+ Right pk ->+ return $+ CloudFrontSigningKey + { cfk_key_id = ki+ , cfk_key = pk+ }++-- | If you have the DER ByteString then you can construct a private key+-- functionally.++parseRSAPrivateKeyDER :: LBS.ByteString -> Either String C.PrivateKey+parseRSAPrivateKeyDER bs = + case A.decodeASN1 A.DER bs of+ Left err -> Left $ show err+ Right as ->+ case A.fromASN1 as of+ Left err -> Left $ show err+ Right pr -> + case pr of+ (pk,[]) -> Right pk+ _ -> Left "residula data"++-- | In most cases only a time-limited, signed URL is needed, in which case a+-- canned policy can be used; URLs signed with a canned policy are shorter+-- than those signed with a custom policy.++signCannedPolicyURL :: CloudFrontSigningKey -> UTCTime -> URL -> URL+signCannedPolicyURL CloudFrontSigningKey{..} exp_utc url = + printf "%s%cExpires=%s&Signature=%s&Key-Pair-Id=%s" url sep exp_eps pol_sig cfk_key_id + where+ exp_eps = unixTime exp_utc+ pol_sig = b64 $ rsa_sha1 cfk_key pol + pol = cannedPolicy exp_utc url+ sep = if any (=='?') url then '&' else '?'++-- | Signing a URL with a custom policy allows a start time to be specified and+-- the IP address of the recipient(s) to be specified.++signCustomPolicyURL :: CloudFrontSigningKey -> CloudFrontPolicy -> URL+signCustomPolicyURL cfk cfp = signCustomPolicyURL_ cfk (customPolicy cfp) $ cfp_Resource cfp++-- | The URL can also be signed with the custom policy in JSON format.+-- (See the CloudFront documentation for details.)++signCustomPolicyURL_ :: CloudFrontSigningKey -> JSONPOlicy -> URL -> URL+signCustomPolicyURL_ CloudFrontSigningKey{..} pol url =+ printf "%s%cPolicy=%s&Signature=%s&Key-Pair-Id=%s" url sep pol_b64 pol_sig cfk_key_id + where+ pol_sig = b64 $ rsa_sha1 cfk_key pol + pol_b64 = b64 pol+ sep = if any (=='?') url then '&' else '?'++-- | The JSON canned policy can be generated from the expiry time and+-- the URL of the distributed resource.++cannedPolicy :: UTCTime -> URL -> JSONPOlicy+cannedPolicy exp_utc url =+ concat+ [ "{\"Statement\":[{\"Resource\":\""+ , url + , "\",\"Condition\":{\"DateLessThan\":{\"AWS:EpochTime\":"+ , unixTime exp_utc+ , "}}}]}"+ ]++-- | JSON custom policies provide more flexibility (allowing start times and+-- recipient IP addresses to be specified) but generate longer signed URLs.++customPolicy :: CloudFrontPolicy -> JSONPOlicy+customPolicy CloudFrontPolicy{..} = unlines $ catMaybes+ [ ok $ "{"+ , ok $ " \"Statement\": [{"+ , ok $ " \"Resource\":\"" ++ cfp_Resource ++ "\","+ , ok $ " \"Condition\":{"+ , ok $ " \"DateLessThan\":{\"AWS:EpochTime\":" ++ unixTime cfp_DateLessThan ++ "},"+ , st $ \ust -> " \"DateGreaterThan\":{\"AWS:EpochTime\":" ++ unixTime ust ++ "},"+ , ok $ " \"IpAddress\":{\"AWS:SourceIp\":\"" ++ maybe "0.0.0.0/0" id cfp_IpAddress ++"\"}"+ , ok $ " }"+ , ok $ " }]"+ , ok $ "}"+ ]+ where+ ok = Just+ st f = maybe Nothing (Just . f) cfp_DateGreaterThan++-- | CloudFront uses Unix Epoch time (number of seconds since 1970, UTC) to+-- specify UTC.++unixTime :: UTCTime -> String+unixTime = formatTime defaultTimeLocale "%s" ++rsa_sha1 :: PrivateKey -> String -> String+rsa_sha1 pk = LBS.unpack . rsassa_pkcs1_v1_5_sign ha_SHA1 pk . LBS.pack++b64 :: String -> String+b64 = map f . LBS.unpack . B64.encode . LBS.pack+ where+ f '+' = '-'+ f '=' = '_'+ f '/' = '~'+ f c = c