Skip to content

Commit d8e3375

Browse files
authored
Merge pull request #264 from squalus/fingerprint
core: add fingerprints
2 parents c1d45dc + 12fa758 commit d8e3375

File tree

3 files changed

+120
-0
lines changed

3 files changed

+120
-0
lines changed

hnix-store-core/hnix-store-core.cabal

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ library
6262
, System.Nix.ContentAddress
6363
, System.Nix.Derivation
6464
, System.Nix.DerivedPath
65+
, System.Nix.Fingerprint
6566
, System.Nix.Hash
6667
, System.Nix.Hash.Truncation
6768
, System.Nix.Signature
@@ -98,6 +99,7 @@ test-suite core
9899
main-is: Driver.hs
99100
other-modules:
100101
Derivation
102+
Fingerprint
101103
Hash
102104
Signature
103105
hs-source-dirs:
@@ -111,10 +113,13 @@ test-suite core
111113
, base16-bytestring
112114
, base64-bytestring
113115
, bytestring
116+
, containers
114117
, crypton
115118
, data-default-class
116119
, hspec
117120
, tasty
118121
, tasty-golden
119122
, tasty-hspec
120123
, text
124+
, time
125+
, unordered-containers
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
{-# LANGUAGE OverloadedStrings #-}
2+
3+
{-|
4+
Description : Fingerprint of Nix store path metadata used for signature verification
5+
-}
6+
module System.Nix.Fingerprint
7+
( fingerprint
8+
, metadataFingerprint
9+
) where
10+
11+
import Crypto.Hash (Digest)
12+
import Data.Dependent.Sum (DSum)
13+
import Data.List (sort)
14+
import Data.Maybe (fromMaybe)
15+
import Data.Text (Text)
16+
import Data.Text.Lazy (toStrict)
17+
import Data.Text.Lazy.Builder (toLazyText)
18+
import Data.Word (Word64)
19+
import System.Nix.Hash (HashAlgo, algoDigestBuilder)
20+
import System.Nix.StorePath
21+
import System.Nix.StorePath.Metadata (Metadata(..))
22+
23+
import qualified Data.HashSet as HashSet
24+
import qualified Data.Text as Text
25+
26+
-- | Produce the message signed by a NAR signature
27+
metadataFingerprint :: StoreDir -> StorePath -> Metadata StorePath -> Text
28+
metadataFingerprint storeDir storePath Metadata{..} = let
29+
narSize = fromMaybe 0 narBytes
30+
in fingerprint storeDir storePath narHash narSize (HashSet.toList references)
31+
32+
-- | Produce the message signed by a NAR signature
33+
fingerprint :: StoreDir
34+
-> StorePath
35+
-> DSum HashAlgo Digest -- ^ NAR hash
36+
-> Word64 -- ^ NAR size, in bytes
37+
-> [StorePath] -- ^ References
38+
-> Text
39+
fingerprint storeDir storePath narHash narSize refs = let
40+
encodedStorePath = storePathToText storeDir storePath
41+
encodedNarHash = (toStrict . toLazyText . algoDigestBuilder) narHash
42+
encodedNarSize = (Text.pack . show) narSize
43+
sortedRefs = sort (storePathToText storeDir <$> refs)
44+
encodedRefs = Text.intercalate "," sortedRefs
45+
in Text.intercalate ";" [ "1", encodedStorePath, encodedNarHash, encodedNarSize, encodedRefs]
46+
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
{-# LANGUAGE OverloadedStrings #-}
2+
-- Test case from https://code.tvl.fyi/commit/tvix/nix-compat/src/narinfo/fingerprint.rs?id=a834966efd64c1b2306241c3ef20f4258f6b9c4e
3+
4+
module Fingerprint where
5+
6+
import Crypto.Error (CryptoFailable(..))
7+
import Data.Default.Class
8+
import System.Nix.Base (decodeWith, BaseEncoding(Base64))
9+
import System.Nix.Fingerprint
10+
import System.Nix.Signature
11+
import System.Nix.StorePath
12+
import System.Nix.StorePath.Metadata
13+
import System.Nix.Hash (mkNamedDigest)
14+
import Data.Text (Text)
15+
import Data.Time.Clock (UTCTime(..))
16+
import Data.Time.Calendar.OrdinalDate (fromOrdinalDate)
17+
import Test.Hspec
18+
19+
import qualified Crypto.PubKey.Ed25519 as Ed25519
20+
import qualified Data.HashSet as HashSet
21+
import qualified Data.Set as Set
22+
import qualified Data.Text.Encoding as Text
23+
24+
spec_fingerprint :: Spec
25+
spec_fingerprint = do
26+
27+
describe "fingerprint" $ do
28+
29+
it "is valid for example metadata" $
30+
metadataFingerprint def exampleStorePath exampleMetadata `shouldBe` exampleFingerprint
31+
32+
it "allows a successful signature verification" $ do
33+
let msg = Text.encodeUtf8 $ metadataFingerprint def exampleStorePath exampleMetadata
34+
Signature sig' = head $ sig <$> filter (\(NarSignature publicKey _) -> publicKey == "cache.nixos.org-1") (Set.toList (sigs exampleMetadata))
35+
sig' `shouldSatisfy` Ed25519.verify pubkey msg
36+
37+
exampleFingerprint :: Text
38+
exampleFingerprint = "1;/nix/store/syd87l2rxw8cbsxmxl853h0r6pdwhwjr-curl-7.82.0-bin;sha256:1b4sb93wp679q4zx9k1ignby1yna3z7c4c2ri3wphylbc2dwsys0;196040;/nix/store/0jqd0rlxzra1rs38rdxl43yh6rxchgc6-curl-7.82.0,/nix/store/6w8g7njm4mck5dmjxws0z1xnrxvl81xa-glibc-2.34-115,/nix/store/j5jxw3iy7bbz4a57fh9g2xm2gxmyal8h-zlib-1.2.12,/nix/store/yxvjs9drzsphm9pcf42a4byzj1kb9m7k-openssl-1.1.1n";
39+
40+
exampleStorePath :: StorePath
41+
exampleStorePath = forceRight $ parsePath def "/nix/store/syd87l2rxw8cbsxmxl853h0r6pdwhwjr-curl-7.82.0-bin"
42+
43+
exampleMetadata :: Metadata StorePath
44+
exampleMetadata = Metadata
45+
{ deriverPath = Just $ forceRight $ parsePath def "/nix/store/5rwxzi7pal3qhpsyfc16gzkh939q1np6-curl-7.82.0.drv"
46+
, narHash = forceRight $ mkNamedDigest "sha256" "1b4sb93wp679q4zx9k1ignby1yna3z7c4c2ri3wphylbc2dwsys0"
47+
, references = HashSet.fromList $ forceRight . parsePath def <$> ["/nix/store/0jqd0rlxzra1rs38rdxl43yh6rxchgc6-curl-7.82.0","/nix/store/6w8g7njm4mck5dmjxws0z1xnrxvl81xa-glibc-2.34-115","/nix/store/j5jxw3iy7bbz4a57fh9g2xm2gxmyal8h-zlib-1.2.12","/nix/store/yxvjs9drzsphm9pcf42a4byzj1kb9m7k-openssl-1.1.1n"]
48+
, registrationTime = UTCTime (fromOrdinalDate 0 0) 0
49+
, narBytes = Just 196040
50+
, trust = BuiltElsewhere
51+
, sigs = Set.fromList $ forceRight . parseSignature <$> ["cache.nixos.org-1:TsTTb3WGTZKphvYdBHXwo6weVILmTytUjLB+vcX89fOjjRicCHmKA4RCPMVLkj6TMJ4GMX3HPVWRdD1hkeKZBQ==", "test1:519iiVLx/c4Rdt5DNt6Y2Jm6hcWE9+XY69ygiWSZCNGVcmOcyL64uVAJ3cV8vaTusIZdbTnYo9Y7vDNeTmmMBQ=="]
52+
, contentAddress = Nothing
53+
}
54+
55+
pubkey :: Ed25519.PublicKey
56+
pubkey = forceDecodeB64Pubkey "6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY="
57+
58+
forceDecodeB64Pubkey :: Text -> Ed25519.PublicKey
59+
forceDecodeB64Pubkey b64EncodedPubkey = let
60+
decoded = forceRight $ decodeWith Base64 b64EncodedPubkey
61+
in case Ed25519.publicKey decoded of
62+
CryptoFailed err -> (error . show) err
63+
CryptoPassed x -> x
64+
65+
forceRight :: Either a b -> b
66+
forceRight = \case
67+
Right x -> x
68+
_ -> error "fromRight failed"
69+

0 commit comments

Comments
 (0)