summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorClint Adams <clint@softwarefreedom.org>2012-04-26 16:29:50 -0400
committerClint Adams <clint@softwarefreedom.org>2012-04-26 16:29:50 -0400
commit716dd382221343e5da5daf1a9a8ac40304a7d74b (patch)
tree0a2b7bed56f9ec6564d4a9e918ab4e8fd53c0dc5
parentd17ea2bb11255f7e6ae9069d3b58e8f06d5946a5 (diff)
Codec for "Cleartext Signatures"
-rw-r--r--Codec/Encryption/OpenPGP/ASCIIArmor/Decode.hs32
-rw-r--r--Codec/Encryption/OpenPGP/ASCIIArmor/Encode.hs10
-rw-r--r--Codec/Encryption/OpenPGP/ASCIIArmor/Types.hs2
-rw-r--r--Codec/Encryption/OpenPGP/ASCIIArmor/Utils.hs17
-rw-r--r--openpgp-asciiarmor.cabal9
-rw-r--r--tests/data/msg39
-rw-r--r--tests/data/msg3.asc29
-rw-r--r--tests/data/msg3.sigbin0 -> 543 bytes
-rw-r--r--tests/data/msg41
-rw-r--r--tests/data/msg4.asc17
-rw-r--r--tests/data/msg4.sigbin0 -> 543 bytes
-rw-r--r--tests/suite.hs35
12 files changed, 154 insertions, 7 deletions
diff --git a/Codec/Encryption/OpenPGP/ASCIIArmor/Decode.hs b/Codec/Encryption/OpenPGP/ASCIIArmor/Decode.hs
index 0376abc..b0033a8 100644
--- a/Codec/Encryption/OpenPGP/ASCIIArmor/Decode.hs
+++ b/Codec/Encryption/OpenPGP/ASCIIArmor/Decode.hs
@@ -10,7 +10,8 @@ module Codec.Encryption.OpenPGP.ASCIIArmor.Decode (
10) where 10) where
11 11
12import Codec.Encryption.OpenPGP.ASCIIArmor.Types 12import Codec.Encryption.OpenPGP.ASCIIArmor.Types
13import Control.Applicative (many, (<|>), (<$>), Alternative, (*>)) 13import Codec.Encryption.OpenPGP.ASCIIArmor.Utils
14import Control.Applicative (many, (<|>), (<$>), Alternative, (<*), (<*>), (*>), optional)
14import Data.Attoparsec.ByteString (Parser, many1, string, inClass, notInClass, satisfy, word8, (<?>), parse, IResult(..)) 15import Data.Attoparsec.ByteString (Parser, many1, string, inClass, notInClass, satisfy, word8, (<?>), parse, IResult(..))
15import Data.Attoparsec.ByteString.Char8 (isDigit_w8, anyChar) 16import Data.Attoparsec.ByteString.Char8 (isDigit_w8, anyChar)
16import Data.Attoparsec.Combinator (manyTill) 17import Data.Attoparsec.Combinator (manyTill)
@@ -37,8 +38,21 @@ parseArmors :: Parser [Armor]
37parseArmors = many parseArmor 38parseArmors = many parseArmor
38 39
39parseArmor :: Parser Armor 40parseArmor :: Parser Armor
40parseArmor = do 41parseArmor = prefixed (clearsigned <|> armor) <?> "armor"
41 atype <- prefixed beginLine <?> "begin line" 42
43clearsigned :: Parser Armor
44clearsigned = do
45 string "-----BEGIN PGP SIGNED MESSAGE-----" <?> "clearsign header"
46 lineEnding <?> "line ending"
47 headers <- armorHeaders <?> "clearsign headers"
48 blankishLine <?> "blank line"
49 cleartext <- dashEscapedCleartext
50 sig <- armor
51 return $ ClearSigned headers cleartext sig
52
53armor :: Parser Armor
54armor = do
55 atype <- beginLine <?> "begin line"
42 headers <- armorHeaders <?> "headers" 56 headers <- armorHeaders <?> "headers"
43 blankishLine <?> "blank line" 57 blankishLine <?> "blank line"
44 payload <- base64Data <?> "base64 data" 58 payload <- base64Data <?> "base64 data"
@@ -84,7 +98,7 @@ armorHeader = do
84 w8sToString = BC8.unpack . B.pack 98 w8sToString = BC8.unpack . B.pack
85 99
86blankishLine :: Parser ByteString 100blankishLine :: Parser ByteString
87blankishLine = many (satisfy (inClass " \t")) >> lineEnding 101blankishLine = many (satisfy (inClass " \t")) *> lineEnding
88 102
89endLine :: ArmorType -> Parser ByteString 103endLine :: ArmorType -> Parser ByteString
90endLine atype = do 104endLine atype = do
@@ -138,3 +152,13 @@ d24 = do
138 152
139prefixed :: Parser a -> Parser a 153prefixed :: Parser a -> Parser a
140prefixed end = end <|> anyChar *> prefixed end 154prefixed end = end <|> anyChar *> prefixed end
155
156dashEscapedCleartext :: Parser ByteString
157dashEscapedCleartext = do
158 ls <- many1 ((deLine <|> unescapedLine) <* lineEnding)
159 return $ crlfUnlines ls
160 where
161 deLine :: Parser ByteString
162 deLine = B.pack <$> (string "- " *> many (satisfy (notInClass "\n\r")))
163 unescapedLine :: Parser ByteString
164 unescapedLine = maybe B.empty B.pack <$> optional ((:) <$> satisfy (notInClass "-\n\r") <*> many (satisfy (notInClass "\n\r")))
diff --git a/Codec/Encryption/OpenPGP/ASCIIArmor/Encode.hs b/Codec/Encryption/OpenPGP/ASCIIArmor/Encode.hs
index 28bb3e6..00d9dd3 100644
--- a/Codec/Encryption/OpenPGP/ASCIIArmor/Encode.hs
+++ b/Codec/Encryption/OpenPGP/ASCIIArmor/Encode.hs
@@ -22,6 +22,7 @@ encode = B.concat . map armor
22 22
23armor :: Armor -> ByteString 23armor :: Armor -> ByteString
24armor (Armor atype ahs bs) = beginLine atype `B.append` armorHeaders ahs `B.append` blankLine `B.append` armorData bs `B.append` armorChecksum bs `B.append` endLine atype 24armor (Armor atype ahs bs) = beginLine atype `B.append` armorHeaders ahs `B.append` blankLine `B.append` armorData bs `B.append` armorChecksum bs `B.append` endLine atype
25armor (ClearSigned chs ctxt csig) = BC8.pack "-----BEGIN PGP SIGNED MESSAGE-----\n" `B.append` armorHeaders chs `B.append` blankLine `B.append` dashEscape ctxt `B.append` armor csig
25 26
26blankLine :: ByteString 27blankLine :: ByteString
27blankLine = BC8.singleton '\n' 28blankLine = BC8.singleton '\n'
@@ -57,3 +58,12 @@ wordWrap lw bs
57 58
58armorChecksum :: ByteString -> ByteString 59armorChecksum :: ByteString -> ByteString
59armorChecksum = BC8.cons '=' . armorData . B.tail . runPut . putWord32be . crc24 60armorChecksum = BC8.cons '=' . armorData . B.tail . runPut . putWord32be . crc24
61
62dashEscape :: ByteString -> ByteString
63dashEscape = BC8.unlines . map escapeLine . BC8.lines
64 where
65 escapeLine :: ByteString -> ByteString
66 escapeLine l
67 | BC8.singleton '-' `B.isPrefixOf` l = BC8.pack "- " `B.append` l
68 | BC8.pack "From " `B.isPrefixOf` l = BC8.pack "- " `B.append` l
69 | otherwise = l
diff --git a/Codec/Encryption/OpenPGP/ASCIIArmor/Types.hs b/Codec/Encryption/OpenPGP/ASCIIArmor/Types.hs
index 46416c1..88a03ce 100644
--- a/Codec/Encryption/OpenPGP/ASCIIArmor/Types.hs
+++ b/Codec/Encryption/OpenPGP/ASCIIArmor/Types.hs
@@ -11,7 +11,7 @@ module Codec.Encryption.OpenPGP.ASCIIArmor.Types (
11import Data.ByteString (ByteString) 11import Data.ByteString (ByteString)
12 12
13data Armor = Armor ArmorType [(String, String)] ByteString 13data Armor = Armor ArmorType [(String, String)] ByteString
14 | ClearSigned [(String, String)] String Armor 14 | ClearSigned [(String, String)] ByteString Armor
15 deriving (Show, Eq) 15 deriving (Show, Eq)
16 16
17data ArmorType = ArmorMessage 17data ArmorType = ArmorMessage
diff --git a/Codec/Encryption/OpenPGP/ASCIIArmor/Utils.hs b/Codec/Encryption/OpenPGP/ASCIIArmor/Utils.hs
new file mode 100644
index 0000000..014c8aa
--- /dev/null
+++ b/Codec/Encryption/OpenPGP/ASCIIArmor/Utils.hs
@@ -0,0 +1,17 @@
1-- ASCIIArmor/Utils.hs: OpenPGP (RFC4880) ASCII armor implementation
2-- Copyright Ⓒ 2012 Clint Adams
3-- This software is released under the terms of the ISC license.
4-- (See the LICENSE file).
5
6module Codec.Encryption.OpenPGP.ASCIIArmor.Utils (
7 crlfUnlines
8) where
9
10import Data.ByteString (ByteString)
11import qualified Data.ByteString as B
12import qualified Data.ByteString.Char8 as BC8
13import Data.List (intersperse)
14
15crlfUnlines :: [ByteString] -> ByteString
16crlfUnlines [] = B.empty
17crlfUnlines ss = B.concat $ intersperse (BC8.pack "\r\n") ss
diff --git a/openpgp-asciiarmor.cabal b/openpgp-asciiarmor.cabal
index 9b141c2..afa7010 100644
--- a/openpgp-asciiarmor.cabal
+++ b/openpgp-asciiarmor.cabal
@@ -15,9 +15,15 @@ Extra-source-files: tests/suite.hs
15 , tests/data/msg1a.asc 15 , tests/data/msg1a.asc
16 , tests/data/msg1b.asc 16 , tests/data/msg1b.asc
17 , tests/data/msg1c.asc 17 , tests/data/msg1c.asc
18 , tests/data/msg2.asc
19 , tests/data/msg1.gpg 18 , tests/data/msg1.gpg
19 , tests/data/msg2.asc
20 , tests/data/msg2.pgp 20 , tests/data/msg2.pgp
21 , tests/data/msg3
22 , tests/data/msg3.asc
23 , tests/data/msg3.sig
24 , tests/data/msg4
25 , tests/data/msg4.asc
26 , tests/data/msg4.sig
21 27
22Cabal-version: >= 1.10 28Cabal-version: >= 1.10
23 29
@@ -29,6 +35,7 @@ Library
29 , Codec.Encryption.OpenPGP.ASCIIArmor.Types 35 , Codec.Encryption.OpenPGP.ASCIIArmor.Types
30 Other-Modules: Data.Digest.CRC24 36 Other-Modules: Data.Digest.CRC24
31 , Codec.Encryption.OpenPGP.ASCIIArmor.Multipart 37 , Codec.Encryption.OpenPGP.ASCIIArmor.Multipart
38 , Codec.Encryption.OpenPGP.ASCIIArmor.Utils
32 Build-depends: attoparsec 39 Build-depends: attoparsec
33 , base > 4 && < 5 40 , base > 4 && < 5
34 , base64-bytestring 41 , base64-bytestring
diff --git a/tests/data/msg3 b/tests/data/msg3
new file mode 100644
index 0000000..e9d15c8
--- /dev/null
+++ b/tests/data/msg3
@@ -0,0 +1,9 @@
1This is a message that will be clearsigned.
2
3From RFC4880, we know that some of these lines should be
4dash-escaped.
5
6-Lines starting with a minus-hyphen MUST be escaped.
7- Lines starting with "From" SHOULD be escaped.
8
9Other lines MAY be escaped.
diff --git a/tests/data/msg3.asc b/tests/data/msg3.asc
new file mode 100644
index 0000000..6d90d30
--- /dev/null
+++ b/tests/data/msg3.asc
@@ -0,0 +1,29 @@
1-----BEGIN PGP SIGNED MESSAGE-----
2Hash: SHA1
3
4This is a message that will be clearsigned.
5
6- From RFC4880, we know that some of these lines should be
7dash-escaped.
8
9- -Lines starting with a minus-hyphen MUST be escaped.
10- - Lines starting with "From" SHOULD be escaped.
11
12Other lines MAY be escaped.
13-----BEGIN PGP SIGNATURE-----
14Version: OpenPrivacy 0.99
15
16iQIcBAEBAgAGBQJPmXqrAAoJEN/7iwtcb1WCG3AQAJ6TBeX12YDI1f/AdtV46quG
17augJYpYZvBbKESGXue1Nv22a7uH4h8LgWRsaEQxMBUwJvlMJfNkjEMAkXQbkj/Og
18J+78bAGMV1GtC5MuwPr8E+M8Z/uHhbzj3fWuUask0Q057u655YIEdlnY4OcZv9jW
19hT+/2kNcC8aw9+kg0I175XNxwBhRXoRKX6dhyAkRSnz7yuQtGXH7kQJAt7TOxxAb
20dud+u5IJixDPebG+NONPfuW5VB8erByW6UPIy4BQBnaxflSD8qJXxCDMWNzOBlYG
21whKXDmlcVgy3J7ghSh2zcFcZhM30Ng49t6k57HOXR9XnI5dskY45yns2nD4kt58/
227anmscGGj0S3pzoUuFAdVIEvziYDhISs35CmTmNh4r4LVuh4R+Zurt3mbe8O6amm
23ZOWZzPsEDX/13B/DnL//70jVhTXUBqDj6MeNj5XHXVbIlfmIyeVLOIXiT/+u1FFt
24+0ERqwFI152GGJJWlikn5bR6P89Xz+04OeTBdxV1fCGt+hlvN6e5X9K15P16QDiq
254COHMZyIyHbtQr92BIj0P46WNsNZJDaoegHl6xtbq60eV3W+LRvgHNphjE+mdIp0
26EV5lBqDGupGYpHkPjZBg0pqASs0Xd3P7SwkoVimtH7mXCPrL4K+o6X8IwsfEMCGY
27Ddej4NK0eolGoz/1sKB0
28=ZShm
29-----END PGP SIGNATURE-----
diff --git a/tests/data/msg3.sig b/tests/data/msg3.sig
new file mode 100644
index 0000000..67ceced
--- /dev/null
+++ b/tests/data/msg3.sig
Binary files differ
diff --git a/tests/data/msg4 b/tests/data/msg4
new file mode 100644
index 0000000..b69b7b6
--- /dev/null
+++ b/tests/data/msg4
@@ -0,0 +1 @@
This message is detach-signed.
diff --git a/tests/data/msg4.asc b/tests/data/msg4.asc
new file mode 100644
index 0000000..d1968f4
--- /dev/null
+++ b/tests/data/msg4.asc
@@ -0,0 +1,17 @@
1-----BEGIN PGP SIGNATURE-----
2Version: GnuPG v1.4.12 (GNU/Linux)
3
4iQIcBAABAgAGBQJPmZE0AAoJEN/7iwtcb1WC6EcQAK+NpDfsgTJgq+5nhZLZQcDC
5+b+8K5eDn+Z/btFZz1h2mF3Y+MpJdgG5fvSSHsYRWiGuT8OBK5wm3vSYnSr8BeA5
6JUJDdhasF7LVosb0ToNiWLbtj9D9iiqmCaPW56Y2u3Ktv5Y4nOAWZw31OGv21B1c
7ptYVv2iy1qPGgnHxYgM5ib37hzKlkTEGKFNMpfYqsMBZyXiKuLpVhEWelawMnCPJ
83b7loOhYvR1Odmolg0dmcFsay7s3uXC/nze+xqTM0Z1HnMxQVW54aixhOoajDO6V
9p7OUUN5g/PBXHvx/gh7gPqgRCazo93rHSgoP//AoEuljrU4W9iNpCEcE5HTdYx0b
10xh10WxWE4VHI8BVt5qhSzPe8BmFRWz+g9CmsJdmhw45W8XxRJMJwoTWzGPcmHTFk
11JTOgJ9/MKcBvYGsBd0wGYq82DbDPdtwGgh5Aa3nz1dxzLCq7qIhOa3VEYKAhCjTx
12UHWnKZrdjSKz8U4D6CECyxlK5UApPc4jWzn3XYCX8s2F84YW7htduE57Yf60bXom
13aefIbDWef1Q4MOUV10h1gyjXtdSiIHvJ0ItvGKmRiXztjhq6+azYN+4RaXzpF5/N
14pqG/DNeTouzMRSzEoLTQ2oBHB8VIbCnP6J2Ck3wPfJZfc6FyCv0gMPQ5pTToll97
156toTAyOHl3pI/inp7IGj
16=TJqj
17-----END PGP SIGNATURE-----
diff --git a/tests/data/msg4.sig b/tests/data/msg4.sig
new file mode 100644
index 0000000..94483e2
--- /dev/null
+++ b/tests/data/msg4.sig
Binary files differ
diff --git a/tests/suite.hs b/tests/suite.hs
index 6fdd816..838a5f3 100644
--- a/tests/suite.hs
+++ b/tests/suite.hs
@@ -5,6 +5,7 @@ import Test.HUnit
5 5
6import Codec.Encryption.OpenPGP.ASCIIArmor (encode, decode, multipartMerge) 6import Codec.Encryption.OpenPGP.ASCIIArmor (encode, decode, multipartMerge)
7import Codec.Encryption.OpenPGP.ASCIIArmor.Types 7import Codec.Encryption.OpenPGP.ASCIIArmor.Types
8import Codec.Encryption.OpenPGP.ASCIIArmor.Utils
8 9
9import Data.ByteString (ByteString) 10import Data.ByteString (ByteString)
10import qualified Data.ByteString as B 11import qualified Data.ByteString as B
@@ -35,11 +36,39 @@ testArmorMultipartDecode fp target = do
35 where 36 where
36 getPayload (Armor _ _ pl) = pl 37 getPayload (Armor _ _ pl) = pl
37 38
39testClearsignedDecodeBody :: FilePath -> FilePath -> Assertion
40testClearsignedDecodeBody fp target = do
41 bs <- B.readFile $ "tests/data/" ++ fp
42 tbs <- B.readFile $ "tests/data/" ++ target
43 case decode bs of
44 Left e -> assertFailure $ "Decode failed (" ++ e ++ ") on " ++ fp
45 Right [a] -> assertEqual ("for " ++ fp) (convertEndings tbs) (getBody a)
46 where
47 getBody (ClearSigned _ txt _) = txt
48 convertEndings = crlfUnlines . BC8.lines
49
50testClearsignedDecodeSig :: FilePath -> FilePath -> Assertion
51testClearsignedDecodeSig fp target = do
52 bs <- B.readFile $ "tests/data/" ++ fp
53 tbs <- B.readFile $ "tests/data/" ++ target
54 case decode bs of
55 Left e -> assertFailure $ "Decode failed (" ++ e ++ ") on " ++ fp
56 Right [a] -> assertEqual ("for " ++ fp) tbs (getSig a)
57 where
58 getSig (ClearSigned _ _ (Armor _ _ sig)) = sig
59
38testArmorEncode :: [FilePath] -> FilePath -> Assertion 60testArmorEncode :: [FilePath] -> FilePath -> Assertion
39testArmorEncode fps target = do 61testArmorEncode fps target = do
40 bss <- mapM (\fp -> B.readFile $ "tests/data/" ++ fp) fps 62 bss <- mapM (\fp -> B.readFile $ "tests/data/" ++ fp) fps
41 tbs <- B.readFile $ "tests/data/" ++ target 63 tbs <- B.readFile $ "tests/data/" ++ target
42 assertEqual ("literaldata") (encode (map (\bs -> Armor ArmorMessage [("Version","OpenPrivacy 0.99")] bs) bss)) tbs 64 assertEqual ("literaldata") tbs (encode (map (\bs -> Armor ArmorMessage [("Version","OpenPrivacy 0.99")] bs) bss))
65
66testClearsignedEncode :: FilePath -> FilePath -> FilePath -> Assertion
67testClearsignedEncode ftxt fsig ftarget = do
68 txt <- B.readFile $ "tests/data/" ++ ftxt
69 sig <- B.readFile $ "tests/data/" ++ fsig
70 target <- B.readFile $ "tests/data/" ++ ftarget
71 assertEqual ("clearsigned encode") target (encode [ClearSigned [("Hash","SHA1")] txt (Armor ArmorSignature [("Version","OpenPrivacy 0.99")] sig)])
43 72
44tests = [ 73tests = [
45 testGroup "CRC24" [ 74 testGroup "CRC24" [
@@ -51,9 +80,13 @@ tests = [
51 testCase "Decode sample armor" (testArmorDecode "msg1.asc" ["msg1.gpg"]) 80 testCase "Decode sample armor" (testArmorDecode "msg1.asc" ["msg1.gpg"])
52 , testCase "Decode sample armor with cruft" (testArmorDecode "msg1a.asc" ["msg1.gpg"]) 81 , testCase "Decode sample armor with cruft" (testArmorDecode "msg1a.asc" ["msg1.gpg"])
53 , testCase "Decode multiple sample armors" (testArmorDecode "msg1b.asc" ["msg1.gpg","msg1.gpg","msg1.gpg"]) 82 , testCase "Decode multiple sample armors" (testArmorDecode "msg1b.asc" ["msg1.gpg","msg1.gpg","msg1.gpg"])
83 , testCase "Decode detached signature" (testArmorDecode "msg4.asc" ["msg4.sig"])
54 , testCase "Decode multi-part armor" (testArmorMultipartDecode "msg2.asc" "msg2.pgp") 84 , testCase "Decode multi-part armor" (testArmorMultipartDecode "msg2.asc" "msg2.pgp")
85 , testCase "Decode body of clear-signed" (testClearsignedDecodeBody "msg3.asc" "msg3")
86 , testCase "Decode sig of clear-signed" (testClearsignedDecodeSig "msg3.asc" "msg3.sig")
55 , testCase "Encode sample armor" (testArmorEncode ["msg1.gpg"] "msg1.asc") 87 , testCase "Encode sample armor" (testArmorEncode ["msg1.gpg"] "msg1.asc")
56 , testCase "Encode multiple sample armors" (testArmorEncode ["msg1.gpg","msg1.gpg","msg1.gpg"] "msg1c.asc") 88 , testCase "Encode multiple sample armors" (testArmorEncode ["msg1.gpg","msg1.gpg","msg1.gpg"] "msg1c.asc")
89 , testCase "Encode clear-signed sig" (testClearsignedEncode "msg3" "msg3.sig" "msg3.asc")
57 ] 90 ]
58 ] 91 ]
59 92