    module Propellor.Property.DnsSec where

    import Propellor.Base
    import qualified Propellor.Property.File as File

    -- | Puts the DNSSEC key files in place from PrivData.
    --
    -- signedPrimary uses this, so this property does not normally need to be
    -- used directly.
    keysInstalled :: Domain -> RevertableProperty (HasInfo + UnixLike) UnixLike
    keysInstalled domain = setup <!> cleanup
      where
    	setup = propertyList "DNSSEC keys installed" $ toProps $
    		map installkey keys

    	cleanup = propertyList "DNSSEC keys removed" $ toProps $
    		map (File.notPresent . keyFn domain) keys

    	installkey k = writer (keysrc k) (keyFn domain k) (Context domain)
    	  where
    		writer
    			| isPublic k = File.hasPrivContentExposedFrom
    			| otherwise = File.hasPrivContentFrom

    	keys = [ PubZSK, PrivZSK, PubKSK, PrivKSK ]

    	keysrc k = PrivDataSource (DnsSec k) $ unwords
    		[ "The file with extension"
    		, keyExt k
    		, "created by running:"
    		, if isZoneSigningKey k
    			then "dnssec-keygen -a RSASHA256 -b 2048 -n ZONE " ++ domain
    			else "dnssec-keygen -f KSK -a RSASHA256 -b 4096 -n ZONE " ++ domain
    		]

    -- | Uses dnssec-signzone to sign a domain's zone file.
    --
    -- signedPrimary uses this, so this property does not normally need to be
    -- used directly.
    zoneSigned :: Domain -> FilePath -> RevertableProperty (HasInfo + UnixLike) UnixLike
    zoneSigned domain zonefile = setup <!> cleanup
      where
    	setup :: Property (HasInfo + UnixLike)
    	setup = check needupdate (forceZoneSigned domain zonefile)
    		`requires` keysInstalled domain

    	cleanup :: Property UnixLike
    	cleanup = File.notPresent (signedZoneFile zonefile)
    		`before` File.notPresent dssetfile
    		`before` revert (keysInstalled domain)

    	dssetfile = dir </> "-" ++ domain ++ "."
    	dir = takeDirectory zonefile

    	-- Need to update the signed zone file if the zone file or
    	-- any of the keys have a newer timestamp.
    	needupdate = do
    		v <- catchMaybeIO $ getModificationTime (signedZoneFile zonefile)
    		case v of
    			Nothing -> return True
    			Just t1 -> anyM (newerthan t1) $
    				zonefile : map (keyFn domain) [minBound..maxBound]

    	newerthan t1 f = do
    		t2 <- getModificationTime f
    		return (t2 >= t1)

    forceZoneSigned :: Domain -> FilePath -> Property UnixLike
    forceZoneSigned domain zonefile = property ("zone signed for " ++ domain) $ liftIO $ do
    	salt <- take 16 <$> saltSha1
     	let p = proc "dnssec-signzone"
    		[ "-A"
    		, "-3", salt
    		-- The serial number needs to be increased each time the
    		-- zone is resigned, even if there are no other changes,
    		-- so that it will propagate to secondaries. So, use the
    		-- unixtime serial format.
    		, "-N", "unixtime"
    		, "-o", domain
    		, zonefile
    		-- the ordering of these key files does not matter
    		, keyFn domain PubZSK
    		, keyFn domain PubKSK
    		]
    	-- Run in the same directory as the zonefile, so it will
    	-- write the dsset file there.
    	(_, _, _, h) <- createProcess $
    		p { cwd = Just (takeDirectory zonefile) }
    	ifM (checkSuccessProcess h)
    		( return MadeChange
    		, return FailedChange
    		)

    saltSha1 :: IO String
    saltSha1 = readProcess "sh"
    	[ "-c"
    	, "head -c 1024 /dev/urandom | sha1sum | cut -d ' ' -f 1"
    	]

    -- | The file used for a given key.
    keyFn :: Domain -> DnsSecKey -> FilePath
    keyFn domain k =  "/etc/bind/propellor/dnssec" </> concat
    	[ "K" ++ domain ++ "."
    	, if isZoneSigningKey k then "ZSK" else "KSK"
    	, keyExt k
    	]

    -- | These are the extensions that dnssec-keygen looks for.
    keyExt :: DnsSecKey -> String
    keyExt k
    	| isPublic k = ".key"
    	| otherwise = ".private"

    isPublic :: DnsSecKey -> Bool
    isPublic k = k `elem` [PubZSK, PubKSK]

    isZoneSigningKey :: DnsSecKey -> Bool
    isZoneSigningKey k = k `elem` [PubZSK, PrivZSK]

    -- | dnssec-signzone makes a .signed file
    signedZoneFile :: FilePath -> FilePath
    signedZoneFile zonefile = zonefile ++ ".signed"