packages feed

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 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 #-}