hash-tree (empty) → 0.0.0
raw patch · 7 files changed
+682/−0 lines, 7 filesdep +QuickCheckdep +basedep +base64-bytestringsetup-changed
Dependencies added: QuickCheck, base, base64-bytestring, bytestring, containers, cryptonite, hspec, memory
Files
- Data/HashTree.hs +48/−0
- Data/HashTree/Internal.hs +408/−0
- LICENSE +29/−0
- Setup.hs +2/−0
- hash-tree.cabal +46/−0
- test/HashTreeSpec.hs +148/−0
- test/Spec.hs +1/−0
+ Data/HashTree.hs view
@@ -0,0 +1,48 @@+-- | Two-way (binary) Merkle Hash Trees which implements append-only logs and+-- provides both inclusion proof and consistency proof.+-- The API design is inspired by Certificate Transparency defined in RFC 6962.+module Data.HashTree (+ -- * Settings+ Settings+ , defaultSettings+ -- ** Accessors+ , hash0+ , hash1+ , hash2+ -- * Merkle Hash Trees+ , MerkleHashTrees+ -- ** Accessors+ , info+ , size+ , digest+ -- ** Related types+ , TreeSize+ , Index+ -- ** Creating Merkle Hash Trees+ , empty+ , fromList+ -- ** Appending an element+ , add+ -- * Inclusion Proof+ , InclusionProof+ , defaultInclusionProof+ -- ** Accessors+ , leafIndex+ , treeSize+ , inclusion+ -- ** Proof and verification+ , generateInclusionProof+ , verifyInclusionProof+ -- * Consistency Proof+ , ConsistencyProof+ , defaultConsistencyProof+ -- ** Accessors+ , firstTreeSize+ , secondTreeSize+ , consistency+ -- ** Proof and verification+ , generateConsistencyProof+ , verifyConsistencyProof+ ) where++import Data.HashTree.Internal
+ Data/HashTree/Internal.hs view
@@ -0,0 +1,408 @@+{-# LANGUAGE OverloadedStrings #-}++module Data.HashTree.Internal (+ Settings(..)+ , defaultSettings+ , MerkleHashTrees(..)+ , digest+ , info+ , currentHead+ , empty+ , fromList+ , toHashTree+ , add+ , InclusionProof(..)+ , defaultInclusionProof+ , generateInclusionProof+ , verifyInclusionProof+ , ConsistencyProof(..)+ , defaultConsistencyProof+ , TreeSize+ , Index+ , generateConsistencyProof+ , verifyConsistencyProof+ ) where++import Crypto.Hash (Digest, SHA256, HashAlgorithm, hash)+import Data.Bits (testBit, finiteBitSize, countLeadingZeros, (.&.), unsafeShiftR)+import Data.ByteArray (ByteArrayAccess)+import qualified Data.ByteArray as BA+import Data.ByteString (ByteString)+import qualified Data.ByteString as BS+import Data.ByteString.Char8 ()+import Data.List (foldl')+import Data.Map.Strict (Map)+import qualified Data.Map.Strict as Map+import Data.IntMap.Strict (IntMap)+import qualified Data.IntMap.Strict as IntMap++-- $setup+-- >>> :set -XOverloadedStrings++----------------------------------------------------------------++-- | Settings for Merkle Hash Trees.+-- The first parameter is input data type.+-- The second one is digest data type.+--+-- To create this, use 'defaultSettings':+--+-- > defaultSettings { hash0 = ..., hash1 = ..., hash2 = ... }+data Settings inp ha = Settings {+ -- | A hash value for non input element.+ hash0 :: Digest ha+ -- | A hash function for one input element to calculate the leaf digest.+ , hash1 :: inp -> Digest ha+ -- | A hash function for two input elements to calculate the internal digest.+ , hash2 :: Digest ha -> Digest ha -> Digest ha+ }++sha256 :: ByteString -> Digest SHA256+sha256 = hash++-- | A default Settings with 'ByteString' and 'SHA256'.+-- This can be used for CT(Certificate Transparency) defined in RFC 6962.+defaultSettings :: Settings ByteString SHA256+defaultSettings = Settings {+ hash0 = sha256 ""+ , hash1 = \x -> sha256 (BS.singleton 0x00 `BS.append` x)+ , hash2 = \x y -> sha256 $ BS.concat [BS.singleton 0x01, BA.convert x, BA.convert y]+ }++----------------------------------------------------------------++-- | The position of the target element from 0.+type Index = Int++-- | The size of hash tree.+type TreeSize = Int++-- | The data type for Merkle Hash Trees.+-- The first parameter is input data type.+-- The second one is digest data type.+data MerkleHashTrees inp ha = MerkleHashTrees {+ settings :: !(Settings inp ha)+ -- | Getting the log size+ , size :: !TreeSize+ -- index is size of HashTree+ -- 0 for Empty+ -- 1 for Leaf 0 0+ -- 'size' for the last HashTree+ , hashtrees :: !(IntMap (HashTree inp ha)) -- the Int key is TreeSize+ , indices :: !(Map (Digest ha) Index)+ }++-- | Getting the Merkle Tree Hash.+digest :: TreeSize -> MerkleHashTrees inp ha -> Maybe (Digest ha)+digest tsiz mht = case IntMap.lookup tsiz (hashtrees mht) of+ Nothing -> Nothing+ Just ht -> Just $ value ht++currentHead :: MerkleHashTrees inp ha -> Maybe (HashTree inp ha)+currentHead (MerkleHashTrees _ tsiz htdb _) = IntMap.lookup tsiz htdb++-- | Getting the root information of the Merkle Hash Tree.+-- A pair of the current size and the current Merle Tree Hash is returned.+info :: MerkleHashTrees inp ha -> (TreeSize, Digest ha)+info mht = (siz, h)+ where+ siz = size mht+ Just h = digest siz mht++----------------------------------------------------------------++data HashTree inp ha =+ Empty !(Digest ha)+ | Leaf !(Digest ha) !Index inp+ | Node !(Digest ha) !Index !Index !(HashTree inp ha) !(HashTree inp ha)+ deriving (Eq, Show)++-- | Creating an empty 'MerkleHashTrees'.+--+-- >>> info $ empty defaultSettings+-- (0,e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855)+empty :: Settings inp ha -> MerkleHashTrees inp ha+empty set = MerkleHashTrees {+ settings = set+ , size = 0+ , hashtrees = IntMap.insert 0 (Empty (hash0 set)) IntMap.empty+ , indices = Map.empty+ }++value :: HashTree inp ha -> Digest ha+value (Empty ha) = ha+value (Leaf ha _ _) = ha+value (Node ha _ _ _ _) = ha++----------------------------------------------------------------++idxl :: HashTree inp ha -> Index+idxl (Leaf _ i _) = i+idxl (Node _ i _ _ _) = i+idxl _ = error "idxl"++idxr :: HashTree inp ha -> Index+idxr (Leaf _ i _) = i+idxr (Node _ _ i _ _) = i+idxr (Empty _) = error "idxr"++----------------------------------------------------------------++-- | Creating a Merkle Hash Tree from a list of elements. O(n log n)+--+-- >>> info $ fromList defaultSettings ["0","1","2"]+-- (3,725d5230db68f557470dc35f1d8865813acd7ebb07ad152774141decbae71327)+fromList :: (ByteArrayAccess inp, HashAlgorithm ha)+ => Settings inp ha -> [inp] -> MerkleHashTrees inp ha+fromList set xs = foldl' (flip add) (empty set) xs++-- | Adding (appending) an element. O(log n)+--+-- >>> info $ add "1" $ empty defaultSettings+-- (1,2215e8ac4e2b871c2a48189e79738c956c081e23ac2f2415bf77da199dfd920c)+add :: (ByteArrayAccess inp, HashAlgorithm ha)+ => inp -> MerkleHashTrees inp ha -> MerkleHashTrees inp ha+add a mht@(MerkleHashTrees set tsiz htdb idb) =+ case Map.lookup hx idb of+ Just _ -> mht+ Nothing -> case IntMap.lookup tsiz htdb of+ Just ht -> let ht' = newht ht+ htdb' = IntMap.insert tsiz' ht' htdb+ in MerkleHashTrees set tsiz' htdb' idb'+ Nothing -> mht -- never reach+ where+ tsiz' = tsiz + 1+ hx = hash1 set a+ idb' = Map.insert hx tsiz idb++ newht ht = ins ht+ where+ ix = tsiz+ x = Leaf hx ix a++ hash2' = hash2 set+ ins (Empty _) = x+ ins l@(Leaf hl il _ ) = Node (hash2' hl hx) il ix l x+ ins t@(Node h il ir l r)+ | isPowerOf2 sz = Node (hash2' h hx) il ix t x+ | otherwise = let r' = ins r+ h' = hash2' (value l) (value r')+ in Node h' il ix l r'+ where+ sz = ir - il + 1++----------------------------------------------------------------++-- | A simple algorithm to create a binary balanced tree. O(n log n)+-- This is just for testing.+toHashTree :: (ByteArrayAccess inp, HashAlgorithm ha)+ => Settings inp ha -> [inp] -> HashTree inp ha+toHashTree set [] = Empty $ hash0 set -- not used+toHashTree set xs = ht+ where+ toLeaf = uncurry (leaf set)+ leaves = map toLeaf $ zip xs [0..]+ ht = buildup set leaves++leaf :: (ByteArrayAccess inp, HashAlgorithm ha)+ => Settings inp ha -> inp -> Index -> HashTree inp ha+leaf set x i = Leaf (hash1 set x) i x++link :: (ByteArrayAccess inp, HashAlgorithm ha)+ => Settings inp ha -> HashTree inp ha -> HashTree inp ha -> HashTree inp ha+link set l r = Node h (idxl l) (idxr r) l r+ where+ h = hash2 set (value l) (value r)++buildup :: (ByteArrayAccess inp, HashAlgorithm ha)+ => Settings inp ha -> [HashTree inp ha] -> HashTree inp ha+buildup _ [ht] = ht+buildup set hts = buildup set (pairing set hts)++pairing :: (ByteArrayAccess inp, HashAlgorithm ha)+ => Settings inp ha -> [HashTree inp ha] -> [HashTree inp ha]+pairing set (t:u:vs) = link set t u : pairing set vs+pairing _ hts = hts++----------------------------------------------------------------++-- | The type for inclusion proof (aka audit proof).+data InclusionProof ha = InclusionProof {+ -- | The index for the target.+ leafIndex :: !Index+ -- | The hash tree size.+ , treeSize :: !TreeSize+ -- | A list of digest for inclusion.+ , inclusion :: ![Digest ha]+ } deriving (Eq, Show)++-- | The default value for 'InclusionProof' just to create a new value.+defaultInclusionProof :: InclusionProof ha+defaultInclusionProof = InclusionProof {+ leafIndex = 0+ , treeSize = 1+ , inclusion = []+ }++-- | Generating 'InclusionProof' for the target at the server side.+generateInclusionProof :: Digest ha -- ^ The target hash (leaf digest)+ -> TreeSize -- ^ The tree size+ -> MerkleHashTrees inp ha+ -> Maybe (InclusionProof ha)+generateInclusionProof h tsiz (MerkleHashTrees _ _ htdb idb) = do+ ht <- IntMap.lookup tsiz htdb+ i <- Map.lookup h idb+ if i < tsiz then do+ let digests = reverse $ path i ht+ Just $ InclusionProof i tsiz digests+ else+ Nothing+ where+ path m (Node _ _ _ l r)+ | m <= idxr l = value r : path m l+ | otherwise = value l : path m r+ path _ _ = []++-- | Verifying 'InclusionProof' at the client side.+--+-- >>> let target = "3"+-- >>> let mht = fromList defaultSettings ["0","1","2",target,"4","5","6"]+-- >>> let treeSize = 5+-- >>> let leafDigest = hash1 defaultSettings target+-- >>> let Just proof = generateInclusionProof leafDigest treeSize mht+-- >>> let Just rootDigest = digest treeSize mht+-- >>> verifyInclusionProof defaultSettings leafDigest rootDigest proof+-- True+verifyInclusionProof :: (ByteArrayAccess inp, HashAlgorithm ha)+ => Settings inp ha+ -> Digest ha -- ^ The target hash (leaf digest)+ -> Digest ha -- ^ Merkle Tree Hash (root digest) for the tree size+ -> InclusionProof ha -- ^ InclusionProof of the target+ -> Bool+verifyInclusionProof set leafDigest rootDigest (InclusionProof idx tsiz pps)+ | idx >= tsiz = False+ | otherwise = verify (idx,tsiz - 1) leafDigest pps+ where+ verify (_,sn) r [] = sn == 0 && r == rootDigest+ verify (_,0) _ _ = False+ verify fsn@(fn,sn) r (p:ps)+ | fn `testBit` 0 || fn == sn = let r' = hash2 set p r+ fsn' = shiftR1 $ untilSet fsn+ in verify fsn' r' ps+ | otherwise = let r' = hash2 set r p+ fsn' = shiftR1 fsn+ in verify fsn' r' ps++----------------------------------------------------------------++-- | The type for consistency proof.+data ConsistencyProof ha = ConsistencyProof {+ -- | The first hash tree size.+ firstTreeSize :: !TreeSize+ -- | The second hash tree size.+ , secondTreeSize :: !TreeSize+ -- | A list of digest for consistency.+ , consistency :: ![Digest ha]+ } deriving (Eq, Show)++-- | The default value for 'ConsistencyProof' just to create a new value.+defaultConsistencyProof :: ConsistencyProof ha+defaultConsistencyProof = ConsistencyProof {+ firstTreeSize = 1+ , secondTreeSize = 2+ , consistency = []+ }++-- | Generating 'ConsistencyProof' for the target at the server side.+generateConsistencyProof :: TreeSize -> TreeSize -> MerkleHashTrees inp ha -> Maybe (ConsistencyProof ha)+generateConsistencyProof m n (MerkleHashTrees _ _ htdb _)+ | m < 0 || n < 0 = Nothing+ | m > n = Nothing+ | m == 0 = do+ htn <- IntMap.lookup n htdb+ return $ ConsistencyProof m n [value htn]+ | otherwise = do+ htm <- IntMap.lookup m htdb+ htn <- IntMap.lookup n htdb+ let digests = prove htm htn True+ return $ ConsistencyProof m n digests+ where+ prove htm htn flag+ | idxl htm == idxl htn && idxr htm == idxr htn+ = if flag then [] else [value htm]+ prove htm@(Leaf _ _ _) (Node _ _ _ ln rn) flag+ = prove htm ln flag ++ [value rn]+ prove htm@(Node _ midxl midxr lm rm) (Node _ nidxl nidxr ln rn) flag+ | sizm <= k = prove htm ln flag ++ [value rn]+ | otherwise = prove rm rn False ++ [value lm]+ where+ sizm = midxr - midxl + 1+ sizn = nidxr - nidxl + 1+ k = maxPowerOf2 (sizn - 1) -- e.g. if 8, take 4.+ prove _ _ _ = error "generateConsistencyProof:prove"++-- | Verifying 'ConsistencyProof' at the client side.+--+-- >>> let mht0 = fromList defaultSettings ["0","1","2","3"]+-- >>> let (m, digestM) = info mht0+-- >>> let mht1 = add "6" $ add "5" $ add "4" mht0+-- >>> let (n, digestN) = info mht1+-- >>> let Just proof = generateConsistencyProof m n mht1+-- >>> verifyConsistencyProof defaultSettings digestM digestN proof+-- True+verifyConsistencyProof :: (ByteArrayAccess inp, HashAlgorithm ha)+ => Settings inp ha+ -> Digest ha -- start+ -> Digest ha -- end+ -> ConsistencyProof ha+ -> Bool+verifyConsistencyProof set firstHash secondHash (ConsistencyProof first second path)+ | first == 0 = case path of+ [c] -> secondHash == c+ _ -> False+ | first == second = null path && firstHash == secondHash+ | otherwise = case path' of+ [] -> False+ c:cs -> verify (untilNotSet (first - 1, second - 1)) c c cs -- fixme:cs+ where+ path'+ | isPowerOf2 first = firstHash : path+ | otherwise = path+ verify _ fr sr [] = fr == firstHash && sr == secondHash+ verify (_,0) _ _ _ = error "verifyConsistencyProof:verify"+ verify fsn@(fn,sn) fr sr (c:cs)+ | fn `testBit` 0 || fn == sn = let fr' = hash2 set c fr+ sr' = hash2 set c sr+ fsn'+ | not (fn `testBit` 0) = untilSet fsn+ | otherwise = fsn+ fsn'' = shiftR1 fsn'+ in verify fsn'' fr' sr' cs+ | otherwise = let sr' = hash2 set sr c+ fsn' = shiftR1 fsn+ in verify fsn' fr sr' cs++----------------------------------------------------------------++width :: Int -> Int+width x = finiteBitSize x - countLeadingZeros x++isPowerOf2 :: Int -> Bool+isPowerOf2 n = (n .&. (n - 1)) == 0++maxPowerOf2 :: Int -> Int+maxPowerOf2 n = 2 ^ (width n - 1)++shiftR1 :: (Int,Int) -> (Int,Int)+shiftR1 (x,y) = (x `unsafeShiftR` 1, y `unsafeShiftR` 1)++untilNotSet :: (Int,Int) -> (Int,Int)+untilNotSet fsn@(fn,_)+ | fn `testBit` 0 = untilNotSet $ shiftR1 fsn+ | otherwise = fsn++untilSet :: (Int,Int) -> (Int,Int)+untilSet fsn@(fn,_)+ | fn == 0 = fsn+ | fn `testBit` 0 = fsn+ | otherwise = untilSet $ shiftR1 fsn
+ LICENSE view
@@ -0,0 +1,29 @@+Copyright (c) 2017, IIJ Innovation Institute Inc.+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 copyright holders 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 OWNER 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.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ hash-tree.cabal view
@@ -0,0 +1,46 @@+Name: hash-tree+Version: 0.0.0+Author: Kazu Yamamoto <kazu@iij.ad.jp>+Maintainer: Kazu Yamamoto <kazu@iij.ad.jp>+License: BSD3+License-File: LICENSE+Synopsis: Merkle Hash Tree+Description: Purely functional Merkle hash tree which+ implements appe nd-only logs and+ provides both inclusion proof and consistency proof.+Category: Data+Cabal-Version: >= 1.10+Build-Type: Simple++Library+ Default-Language: Haskell2010+ GHC-Options: -Wall+ Exposed-Modules: Data.HashTree+ Other-Modules: Data.HashTree.Internal+ Build-Depends: base >= 4 && < 5+ , bytestring+ , containers+ , cryptonite+ , memory++Test-Suite spec+ Type: exitcode-stdio-1.0+ Default-Language: Haskell2010+ HS-Source-Dirs: test, .+ Ghc-Options: -Wall+ Main-Is: Spec.hs+ Other-Modules: HashTreeSpec+ Data.HashTree.Internal+ Build-Depends: base >= 4 && < 5+ , QuickCheck+ , bytestring+ , base64-bytestring+ , containers+ , cryptonite+ , hspec+ , memory++Source-Repository head+ Type: git+ Location: https://github.com/kazu-yamamoto/hash-tree+
+ test/HashTreeSpec.hs view
@@ -0,0 +1,148 @@+{-# OPTIONS_GHC -fno-warn-orphans #-}+{-# LANGUAGE OverloadedStrings #-}++module HashTreeSpec where++import Crypto.Hash+import Data.ByteString (ByteString)+import qualified Data.ByteString as BS+import Data.ByteString.Base64+import Data.ByteString.Char8 ()+import Data.HashTree+import Data.HashTree.Internal+import Data.List (nub)+import Data.Maybe+import Test.Hspec+import Test.Hspec.QuickCheck+import Test.QuickCheck+++newtype Input = Input [ByteString] deriving Show -- non empty list++instance Arbitrary ByteString where+ arbitrary = BS.pack <$> listOf arbitrary -- Gen Word8++instance Arbitrary Input where+ arbitrary = Input . getNonEmpty <$> arbitrary -- Gen (NonEmptyList ByteString)++spec :: Spec+spec = do+ let set = defaultSettings+ describe "info" $ do+ prop "The root hash of 1-size tree is equal to the leaf hash" $ \bs ->+ let mht = add (bs :: ByteString) $ empty set+ h1 = snd $ info mht+ h2 = hash1 set bs+ in h1 == h2+ describe "fromList" $ do+ prop "create a perfectly branched tree" $ \(Input bss) ->+ let Just ht = currentHead $ fromList set bss+ ht' = toHashTree set $ nub bss+ in ht == ht'+ describe "verifyInclusionProof" $ do+ it "can verify the certificate transparency" $+ verifyInclusionProof set dl d2 iProof `shouldBe` True+ prop "can verify for a good target" $ \(Input bss0) x0 y0 ->+ let bss = nub bss0+ mht = fromList set bss+ len = length bss+ x0' = adjust x0 len+ y0' = adjust y0 len+ (i,tsiz)+ | x0' < y0' = (x0',y0')+ | x0' > y0' = (y0',x0')+ | otherwise = (x0',y0'+1)+ target = bss !! i+ leafDigest = hash1 set target+ proof = fromJust $ generateInclusionProof leafDigest tsiz mht+ Just rootDigest = digest tsiz mht+ in verifyInclusionProof set leafDigest rootDigest proof+ describe "verifyConsistencyProof" $ do+ it "can verify the certificate transparency" $+ verifyConsistencyProof set d1 d2 cProof `shouldBe` True+ prop "can verify" $ \(Input bss) m0 n0 ->+ let mht = fromList set bss+ siz = size mht+ m0' = adjust m0 siz+ n0' = adjust n0 siz+ (m,n) = if m0' <= n0' then (m0',n0') else (n0',m0')+ proof = fromJust $ generateConsistencyProof m n mht+ Just dm = digest m mht+ Just dn = digest n mht+ in verifyConsistencyProof set dm dn proof++adjust :: Int -> Int -> Int+adjust x b+ | x < 0 = negate x `mod` b+ | otherwise = x `mod` b++toDigest :: ByteString -> Digest SHA256+toDigest x = d+ where+ Right r = decode x+ Just d = digestFromByteString r+++{-+% wget https://ct.googleapis.com/icarus/ct/v1/get-sth+{"tree_size":129596132,"timestamp":1508198131197,"sha256_root_hash":"qyJv/TObheataX8uFjAJsR+dJtJgNlfjzTe5CX8Ej2s=","tree_head_signature":"BAMASDBGAiEAhtcpfpVSKRuZOxEMpPRRiu5nPdd8UL3JnQBk23Zrk/ACIQD+n4YSigh3o1KaJvBTvJkcmq2CW5QtrIBdI2oDiItN1A=="}+-}+d1 :: Digest SHA256+d1 = toDigest "qyJv/TObheataX8uFjAJsR+dJtJgNlfjzTe5CX8Ej2s="++{-+% wget https://ct.googleapis.com/icarus/ct/v1/get-sth+{"tree_size":129721434,"timestamp":1508216080417,"sha256_root_hash":"jhnkzC7e95jLNBJSiQRnuH2sXYfpY6sASO3ezfUBU0w=","tree_head_signature":"BAMARzBFAiBkiNQdBXnYQLwbcSxPYjvKYatziAbADq124OifIeZ2oQIhAOzYM60DNII1ILtWDMvCPcGqTapkb/Ru4KIJJwdTi+dg"}+-}+d2 :: Digest SHA256+d2 = toDigest "jhnkzC7e95jLNBJSiQRnuH2sXYfpY6sASO3ezfUBU0w="++{-+% wget 'https://ct.googleapis.com/icarus/ct/v1/get-sth-consistency?first=129596132&second=129721434'+{"consistency":["5mrKRX9GRLriSIAtkX0kdvlnjt/f+P0vTrhmzOdhnzs=","VEqL3Z55VZTVRp45WX7gTdcwxOorltn+uCQpHjv9rjE=","2Euj4VXixjEAGj9p23OMo+ciB1tQ2KXH97TkWgcZLpI=","rG0l6AHdk3TRkgAnwTK1Ys5kU7uuKRkMQSkrES0ocj0=","JSxii5BAGCuwifRNIgIsarVA/aSC/QJNv4kfT9zii3Y=","WPaErDHN/lYCuv8TsvosmAsIDbEOfPNZ/LxqLvuQcdY=","HjN1fCoS6OYw8VH+DAcQPQkCjksWljkvr3AJiZTsi6Y=","liuu+DeQohOzAyQadR3imE0SLo8pSsv9K0Y9f1dGrwA=","oUQV+oip0wozcwv4mmMv4MrP545ugwXDIMU6HkZrfl0=","V9BPc7aq1Sq1XwFV3gMx7/9Wv+8l/81S5Gb+bDriRZE=","h3PJZDxVUsGMPBMEaJsQnqAVjlpuxUPGAUfYCY6TkWY=","Rj09QnLzZ2MmyKjJYkcSO8Ko6lGhgwrnOydn+RdHdeI=","Dc7oYAljc9VEYI8JuvYVyJtTHP8uW7igIKVqnWXgvFs=","tJfBVa+YE0GK8T1L74hkkGmT3QO+mznMf+XrKnS3YOE=","V9uRJUhCpCMwss1i4c85X5ceJnNePXdjAVrxtU6EezY=","nf6Yq8xIs56hgor/mk30dvpaUO1/1w+MUlz8thiEb3o=","SEVW3kh8HQ9Za+LjCoGaAkbbUfYmiwNs3SDasWKS6xo=","96r7XxdWTPAZSygfov9mTTd4Gs6chZfdkznd8HAmzKc=","t6OBvH84uFTwEubekO8rdcv+AlqPcpuZX9pwLYopKGg=","nl6nrmkzk5qSYBiUHN609SD8IH3HuTPYR6r1YWKtWag=","NbPA38XBZUXC5ca/cNcU/mNhq31qg2okTS3kJdptnxg=","Ur4mOpiyDl1Zq/MH/SGszUlBP7jmOfupNVodMT6Mg2M=","LUTvZyrtuEoSL7PNqR2Iv2FbcbtI5lQrq5/97sMFIsU=","Bh+FIkHuIdFAcvCYOTlkKMxR15aDaaT9UlUsrCeuLUY="]}+-}+cProof :: ConsistencyProof SHA256+cProof = ConsistencyProof 129596132 129721434 cs+ where+ bs = ["5mrKRX9GRLriSIAtkX0kdvlnjt/f+P0vTrhmzOdhnzs="+ ,"VEqL3Z55VZTVRp45WX7gTdcwxOorltn+uCQpHjv9rjE="+ ,"2Euj4VXixjEAGj9p23OMo+ciB1tQ2KXH97TkWgcZLpI="+ ,"rG0l6AHdk3TRkgAnwTK1Ys5kU7uuKRkMQSkrES0ocj0="+ ,"JSxii5BAGCuwifRNIgIsarVA/aSC/QJNv4kfT9zii3Y="+ ,"WPaErDHN/lYCuv8TsvosmAsIDbEOfPNZ/LxqLvuQcdY="+ ,"HjN1fCoS6OYw8VH+DAcQPQkCjksWljkvr3AJiZTsi6Y="+ ,"liuu+DeQohOzAyQadR3imE0SLo8pSsv9K0Y9f1dGrwA="+ ,"oUQV+oip0wozcwv4mmMv4MrP545ugwXDIMU6HkZrfl0="+ ,"V9BPc7aq1Sq1XwFV3gMx7/9Wv+8l/81S5Gb+bDriRZE="+ ,"h3PJZDxVUsGMPBMEaJsQnqAVjlpuxUPGAUfYCY6TkWY="+ ,"Rj09QnLzZ2MmyKjJYkcSO8Ko6lGhgwrnOydn+RdHdeI="+ ,"Dc7oYAljc9VEYI8JuvYVyJtTHP8uW7igIKVqnWXgvFs="+ ,"tJfBVa+YE0GK8T1L74hkkGmT3QO+mznMf+XrKnS3YOE="+ ,"V9uRJUhCpCMwss1i4c85X5ceJnNePXdjAVrxtU6EezY="+ ,"nf6Yq8xIs56hgor/mk30dvpaUO1/1w+MUlz8thiEb3o="+ ,"SEVW3kh8HQ9Za+LjCoGaAkbbUfYmiwNs3SDasWKS6xo="+ ,"96r7XxdWTPAZSygfov9mTTd4Gs6chZfdkznd8HAmzKc="+ ,"t6OBvH84uFTwEubekO8rdcv+AlqPcpuZX9pwLYopKGg="+ ,"nl6nrmkzk5qSYBiUHN609SD8IH3HuTPYR6r1YWKtWag="+ ,"NbPA38XBZUXC5ca/cNcU/mNhq31qg2okTS3kJdptnxg="+ ,"Ur4mOpiyDl1Zq/MH/SGszUlBP7jmOfupNVodMT6Mg2M="+ ,"LUTvZyrtuEoSL7PNqR2Iv2FbcbtI5lQrq5/97sMFIsU="+ ,"Bh+FIkHuIdFAcvCYOTlkKMxR15aDaaT9UlUsrCeuLUY="+ ]+ cs = map toDigest bs++{-+% wget 'https://ct.googleapis.com/icarus/ct/v1/get-entry-and-proof?leaf_index=120000000&tree_size=129721434'+{"leaf_input":"AAAAAAFexROk9wAAAAUdMIIFGTCCBAGgAwIBAgISBC/2U32dBHf/8H9aIod2qqPFMA0GCSqGSIb3DQEBCwUAMEoxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MSMwIQYDVQQDExpMZXQncyBFbmNyeXB0IEF1dGhvcml0eSBYMzAeFw0xNzA5MjcxOTQzMDBaFw0xNzEyMjYxOTQzMDBaMBsxGTAXBgNVBAMTEGdlbS1qZXdlbGVycy5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDB61H4Kvny8rX0gBuvPciX9VpPzfkdYHjRwmZLU3uj3awfN+4qouO7ipz9Cyz1saCI53NXisHr3lttxAF1ISnidvywgRyaO9G0BVAn6Tl5zarOhwrf56wopfyEGaHfpyO8dCV5nfNWM99gbmrcJgWqs5/7Np9L+Y0ZMah751X6oZsJqKSs6FCUbAtyeAhXBQMuSOqkd85nObSpnf2W0CAfWYy66VPbDL8OAhq0A/3U+gh9ThWCAPPn9pVghazBKsSL+c1KW84xsXuHWkkgLjJ3f/oL9FPTzNBfqYzzeuAVAEwazYXPyEu5SlbdqzDi1Lesp0+2yoMO9h7MWy9Mom05AgMBAAGjggImMIICIjAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFDQaIW5tEaFbKN9qRmVdZEk2eB4GMB8GA1UdIwQYMBaAFKhKamMEfd265tE5t6ZFZe/zqOyhMG8GCCsGAQUFBwEBBGMwYTAuBggrBgEFBQcwAYYiaHR0cDovL29jc3AuaW50LXgzLmxldHNlbmNyeXB0Lm9yZzAvBggrBgEFBQcwAoYjaHR0cDovL2NlcnQuaW50LXgzLmxldHNlbmNyeXB0Lm9yZy8wMQYDVR0RBCowKIIQZ2VtLWpld2VsZXJzLmNvbYIUd3d3LmdlbS1qZXdlbGVycy5jb20wgf4GA1UdIASB9jCB8zAIBgZngQwBAgEwgeYGCysGAQQBgt8TAQEBMIHWMCYGCCsGAQUFBwIBFhpodHRwOi8vY3BzLmxldHNlbmNyeXB0Lm9yZzCBqwYIKwYBBQUHAgIwgZ4MgZtUaGlzIENlcnRpZmljYXRlIG1heSBvbmx5IGJlIHJlbGllZCB1cG9uIGJ5IFJlbHlpbmcgUGFydGllcyBhbmQgb25seSBpbiBhY2NvcmRhbmNlIHdpdGggdGhlIENlcnRpZmljYXRlIFBvbGljeSBmb3VuZCBhdCBodHRwczovL2xldHNlbmNyeXB0Lm9yZy9yZXBvc2l0b3J5LzANBgkqhkiG9w0BAQsFAAOCAQEAMMIZaoRMUdLpIyc7TmFmjz8h9q24xnwY04J61CALDVV8c8IjzRwuheqcCzOmBAfI4ejBnAb3wxIYIq/FlYmWbLHEdpOviPFgyV6uskNGGAyEWjU+ah3E9NXxJ2+/h/GmDOLk3D6aXeLyFpZr1nAhl9+Sr+Gj/gWijiAhpKKT/al73u4ZG8exhsRlfTr84Mtgn29NwolMAQ5anMuLgk3d7YTOpsi602vQTyEAj5G7SUryBvf+lYhDvyZwLkbEqaQnuUvHu2uLGbtRb002CAptZeqTLQ84hIjS1JJ5YZgdEtPlbUqduuR6CB3nIA6cKdLKyoTw3Yxwh9Xeowe6OEVEIwAA","extra_data":"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","audit_path":["xNXDvcL4JYFTwHXfnl0an0vyCjS0am5+B8dxDozc5ag=","LJc7lPt79J4pgFqY0hbGYXZi401EnFVix2vFUSovrB8=","QFcF13awbePeLAqSfFoNB6cYYU40wwY7aLYQXk/ss/A=","Wlc9EJxIODGOKavf7zXfPOrymuWwviwGUI8K6h9EBFQ=","aigt2QVYyMjXSLFAhuaEvBmqctsFVKFZEgtJYl/gPL8=","DALuGNF8WzgEgdYT34KFCzbRlV+wx5Ie1jrUsfJS+Jo=","wfZyGm8U+u587alYY91O5sOCVVMPqNA1wwst8/CLsWA=","frRFNtjphdfOKw0X1I1AZfU6yuCMZ+/bC9sHXg6L4DQ=","5DgE84Ns18cCD9zOiiPUuUP17nOmxMm4copkw2MHY9g=","ggacmAKa8hnTxehF0WUAbknaQk1Ks6c33x2g/zOVWgw=","I1YeaNL1rb0YNDCb7Xe9VPosJKP0fv3msScVnNsn4PA=","BPVDHS1+mcuIEsShBg19U6UCwIQKJwRZeXIECFA6zcw=","7smS/3DRbnKci59UoBg7xDf+NlAVGAZ2qtG4ILaAK/Q=","75SJNIAzyepGCUnfvUTVrZSddSfWaSxxXXmBiHewa8g=","YxHvS/I+2i1yJLilSU/ZKjyr5jc5ABhuU4Wh6H+oBUY=","7DBov9qK6s/uy8g0iL//5A97ZAELOSE6SjGMGVBK8dY=","JsXCAk0Ztn8YZFmyfUkuJYzAzlV70D+mefthbBvRjaY=","YHBP2MPkXLUeRIMQTdQ9LVxolKbn1onla0Uedgp1x+Y=","yE4z+aQ1n/7sLJRG8jM5xKLq+JlHg3GoN3ApPovnh/o=","fwU2Yg7p+8ACX6v3e7xiJGwDeYo0b+33hbxy+KMdMfA=","5gj0c1LVruisvtT8VA1J9IeWyRaTf0+dwuLnamseTl8=","HQec1yNY1oQtctj/ZfpEtDZZf3aT19Qdo4aaBPvaT+I=","FPtmqvOXlhE5gi/rc/sfWQpvLT7tPfHKJiSCEgmf3Hk=","EtTWrgka0wMLdqJ4oFojyDzjsuLoSyZpyzKzff+yFxU=","Ur4mOpiyDl1Zq/MH/SGszUlBP7jmOfupNVodMT6Mg2M=","LUTvZyrtuEoSL7PNqR2Iv2FbcbtI5lQrq5/97sMFIsU=","Bh+FIkHuIdFAcvCYOTlkKMxR15aDaaT9UlUsrCeuLUY="]}+-}++dl :: Digest SHA256+dl = hash1 defaultSettings r+ where+ Right r = decode "AAAAAAFexROk9wAAAAUdMIIFGTCCBAGgAwIBAgISBC/2U32dBHf/8H9aIod2qqPFMA0GCSqGSIb3DQEBCwUAMEoxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MSMwIQYDVQQDExpMZXQncyBFbmNyeXB0IEF1dGhvcml0eSBYMzAeFw0xNzA5MjcxOTQzMDBaFw0xNzEyMjYxOTQzMDBaMBsxGTAXBgNVBAMTEGdlbS1qZXdlbGVycy5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDB61H4Kvny8rX0gBuvPciX9VpPzfkdYHjRwmZLU3uj3awfN+4qouO7ipz9Cyz1saCI53NXisHr3lttxAF1ISnidvywgRyaO9G0BVAn6Tl5zarOhwrf56wopfyEGaHfpyO8dCV5nfNWM99gbmrcJgWqs5/7Np9L+Y0ZMah751X6oZsJqKSs6FCUbAtyeAhXBQMuSOqkd85nObSpnf2W0CAfWYy66VPbDL8OAhq0A/3U+gh9ThWCAPPn9pVghazBKsSL+c1KW84xsXuHWkkgLjJ3f/oL9FPTzNBfqYzzeuAVAEwazYXPyEu5SlbdqzDi1Lesp0+2yoMO9h7MWy9Mom05AgMBAAGjggImMIICIjAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFDQaIW5tEaFbKN9qRmVdZEk2eB4GMB8GA1UdIwQYMBaAFKhKamMEfd265tE5t6ZFZe/zqOyhMG8GCCsGAQUFBwEBBGMwYTAuBggrBgEFBQcwAYYiaHR0cDovL29jc3AuaW50LXgzLmxldHNlbmNyeXB0Lm9yZzAvBggrBgEFBQcwAoYjaHR0cDovL2NlcnQuaW50LXgzLmxldHNlbmNyeXB0Lm9yZy8wMQYDVR0RBCowKIIQZ2VtLWpld2VsZXJzLmNvbYIUd3d3LmdlbS1qZXdlbGVycy5jb20wgf4GA1UdIASB9jCB8zAIBgZngQwBAgEwgeYGCysGAQQBgt8TAQEBMIHWMCYGCCsGAQUFBwIBFhpodHRwOi8vY3BzLmxldHNlbmNyeXB0Lm9yZzCBqwYIKwYBBQUHAgIwgZ4MgZtUaGlzIENlcnRpZmljYXRlIG1heSBvbmx5IGJlIHJlbGllZCB1cG9uIGJ5IFJlbHlpbmcgUGFydGllcyBhbmQgb25seSBpbiBhY2NvcmRhbmNlIHdpdGggdGhlIENlcnRpZmljYXRlIFBvbGljeSBmb3VuZCBhdCBodHRwczovL2xldHNlbmNyeXB0Lm9yZy9yZXBvc2l0b3J5LzANBgkqhkiG9w0BAQsFAAOCAQEAMMIZaoRMUdLpIyc7TmFmjz8h9q24xnwY04J61CALDVV8c8IjzRwuheqcCzOmBAfI4ejBnAb3wxIYIq/FlYmWbLHEdpOviPFgyV6uskNGGAyEWjU+ah3E9NXxJ2+/h/GmDOLk3D6aXeLyFpZr1nAhl9+Sr+Gj/gWijiAhpKKT/al73u4ZG8exhsRlfTr84Mtgn29NwolMAQ5anMuLgk3d7YTOpsi602vQTyEAj5G7SUryBvf+lYhDvyZwLkbEqaQnuUvHu2uLGbtRb002CAptZeqTLQ84hIjS1JJ5YZgdEtPlbUqduuR6CB3nIA6cKdLKyoTw3Yxwh9Xeowe6OEVEIwAA"++iProof :: InclusionProof SHA256+iProof = InclusionProof 120000000 129721434 is+ where+ bs = ["xNXDvcL4JYFTwHXfnl0an0vyCjS0am5+B8dxDozc5ag=","LJc7lPt79J4pgFqY0hbGYXZi401EnFVix2vFUSovrB8=","QFcF13awbePeLAqSfFoNB6cYYU40wwY7aLYQXk/ss/A=","Wlc9EJxIODGOKavf7zXfPOrymuWwviwGUI8K6h9EBFQ=","aigt2QVYyMjXSLFAhuaEvBmqctsFVKFZEgtJYl/gPL8=","DALuGNF8WzgEgdYT34KFCzbRlV+wx5Ie1jrUsfJS+Jo=","wfZyGm8U+u587alYY91O5sOCVVMPqNA1wwst8/CLsWA=","frRFNtjphdfOKw0X1I1AZfU6yuCMZ+/bC9sHXg6L4DQ=","5DgE84Ns18cCD9zOiiPUuUP17nOmxMm4copkw2MHY9g=","ggacmAKa8hnTxehF0WUAbknaQk1Ks6c33x2g/zOVWgw=","I1YeaNL1rb0YNDCb7Xe9VPosJKP0fv3msScVnNsn4PA=","BPVDHS1+mcuIEsShBg19U6UCwIQKJwRZeXIECFA6zcw=","7smS/3DRbnKci59UoBg7xDf+NlAVGAZ2qtG4ILaAK/Q=","75SJNIAzyepGCUnfvUTVrZSddSfWaSxxXXmBiHewa8g=","YxHvS/I+2i1yJLilSU/ZKjyr5jc5ABhuU4Wh6H+oBUY=","7DBov9qK6s/uy8g0iL//5A97ZAELOSE6SjGMGVBK8dY=","JsXCAk0Ztn8YZFmyfUkuJYzAzlV70D+mefthbBvRjaY=","YHBP2MPkXLUeRIMQTdQ9LVxolKbn1onla0Uedgp1x+Y=","yE4z+aQ1n/7sLJRG8jM5xKLq+JlHg3GoN3ApPovnh/o=","fwU2Yg7p+8ACX6v3e7xiJGwDeYo0b+33hbxy+KMdMfA=","5gj0c1LVruisvtT8VA1J9IeWyRaTf0+dwuLnamseTl8=","HQec1yNY1oQtctj/ZfpEtDZZf3aT19Qdo4aaBPvaT+I=","FPtmqvOXlhE5gi/rc/sfWQpvLT7tPfHKJiSCEgmf3Hk=","EtTWrgka0wMLdqJ4oFojyDzjsuLoSyZpyzKzff+yFxU=","Ur4mOpiyDl1Zq/MH/SGszUlBP7jmOfupNVodMT6Mg2M=","LUTvZyrtuEoSL7PNqR2Iv2FbcbtI5lQrq5/97sMFIsU=","Bh+FIkHuIdFAcvCYOTlkKMxR15aDaaT9UlUsrCeuLUY="]+ is = map toDigest bs
+ test/Spec.hs view
@@ -0,0 +1,1 @@+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}