annex.securehashesonly

Cryptographically secure hashes can be forced to be used in a repository,
by setting annex.securehashesonly. This does not prevent the git repository
from containing files with insecure hashes, but it does prevent the content
of such files from being pulled into .git/annex/objects from another
repository.

We want to make sure that at no point does git-annex accept content into
.git/annex/objects that is hashed with an insecure key. Here's how it
was done:

* .git/annex/objects/xx/yy/KEY/ is kept frozen, so nothing can be
  written to it normally
* So every place that writes content must call, thawContent or modifyContent.
  We can audit for these, and be sure we've considered all cases.
* The main functions are moveAnnex, and linkToAnnex; these were made to
  check annex.securehashesonly, and are the main security boundary
  for annex.securehashesonly.
* Most other calls to modifyContent deal with other files in the KEY
  directory (inode cache etc). The other ones that mess with the content
  are:
	- Annex.Direct.toDirectGen, in which content already in the
	  annex directory is moved to the direct mode file, so not relevant.
	- fix and lock, which don't add new content
	- Command.ReKey.linkKey, which manually unlocks it to make a
	  copy.
* All other calls to thawContent appear safe.

Made moveAnnex return a Bool, so checked all callsites and made them
deal with a failure in appropriate ways.

linkToAnnex simply returns LinkAnnexFailed; all callsites already deal
with it failing in appropriate ways.

This commit was sponsored by Riku Voipio.
This commit is contained in:
Joey Hess 2017-02-27 13:01:32 -04:00
parent 0fda7c08d0
commit 07f1e638ee
No known key found for this signature in database
GPG key ID: C910D9222512E3C7
8 changed files with 79 additions and 37 deletions

View file

@ -1,6 +1,6 @@
{- git-annex file content managing {- git-annex file content managing
- -
- Copyright 2010-2015 Joey Hess <id@joeyh.name> - Copyright 2010-2017 Joey Hess <id@joeyh.name>
- -
- Licensed under the GNU GPL version 3 or higher. - Licensed under the GNU GPL version 3 or higher.
-} -}
@ -80,6 +80,7 @@ import qualified Types.Backend
import qualified Backend import qualified Backend
import qualified Database.Keys import qualified Database.Keys
import Types.NumCopies import Types.NumCopies
import Types.Key
import Annex.UUID import Annex.UUID
import Annex.InodeSentinal import Annex.InodeSentinal
import Utility.InodeCache import Utility.InodeCache
@ -307,10 +308,12 @@ getViaTmp' v key action = do
(ok, verification) <- action tmpfile (ok, verification) <- action tmpfile
if ok if ok
then ifM (verifyKeyContent v verification key tmpfile) then ifM (verifyKeyContent v verification key tmpfile)
( ifM (moveAnnex key tmpfile)
( do ( do
moveAnnex key tmpfile
logStatus key InfoPresent logStatus key InfoPresent
return True return True
, return False
)
, do , do
warning "verification of content failed" warning "verification of content failed"
liftIO $ nukeFile tmpfile liftIO $ nukeFile tmpfile
@ -465,9 +468,18 @@ checkDiskSpace' need destdir key alreadythere samefilesystem = ifM (Annex.getSta
- key, and one of them will probably get deleted later. So, adding the - key, and one of them will probably get deleted later. So, adding the
- check here would only raise expectations that git-annex cannot truely - check here would only raise expectations that git-annex cannot truely
- meet. - meet.
-
- May return false, when a particular variety of key is not being
- accepted into the repository. Will display a warning message in this
- case. May also throw exceptions in some cases.
-} -}
moveAnnex :: Key -> FilePath -> Annex () moveAnnex :: Key -> FilePath -> Annex Bool
moveAnnex key src = withObjectLoc key storeobject storedirect moveAnnex key src = ifM (checkSecureHashes key)
( do
withObjectLoc key storeobject storedirect
return True
, return False
)
where where
storeobject dest = ifM (liftIO $ doesFileExist dest) storeobject dest = ifM (liftIO $ doesFileExist dest)
( alreadyhave ( alreadyhave
@ -509,6 +521,16 @@ moveAnnex key src = withObjectLoc key storeobject storedirect
alreadyhave = liftIO $ removeFile src alreadyhave = liftIO $ removeFile src
checkSecureHashes :: Key -> Annex Bool
checkSecureHashes key
| cryptographicallySecure (keyVariety key) = return True
| otherwise = ifM (annexSecureHashesOnly <$> Annex.getGitConfig)
( do
warning $ "annex.securehashesonly blocked adding " ++ formatKeyVariety (keyVariety key) ++ " key to annex objects"
return False
, return True
)
populatePointerFile :: Key -> FilePath -> FilePath -> Annex () populatePointerFile :: Key -> FilePath -> FilePath -> Annex ()
populatePointerFile k obj f = go =<< liftIO (isPointerFile f) populatePointerFile k obj f = go =<< liftIO (isPointerFile f)
where where
@ -526,9 +548,12 @@ data LinkAnnexResult = LinkAnnexOk | LinkAnnexFailed | LinkAnnexNoop
{- Populates the annex object file by hard linking or copying a source {- Populates the annex object file by hard linking or copying a source
- file to it. -} - file to it. -}
linkToAnnex :: Key -> FilePath -> Maybe InodeCache -> Annex LinkAnnexResult linkToAnnex :: Key -> FilePath -> Maybe InodeCache -> Annex LinkAnnexResult
linkToAnnex key src srcic = do linkToAnnex key src srcic = ifM (checkSecureHashes key)
( do
dest <- calcRepo (gitAnnexLocation key) dest <- calcRepo (gitAnnexLocation key)
modifyContent dest $ linkAnnex To key src srcic dest Nothing modifyContent dest $ linkAnnex To key src srcic dest Nothing
, return LinkAnnexFailed
)
{- Makes a destination file be a link or copy from the annex object. -} {- Makes a destination file be a link or copy from the annex object. -}
linkFromAnnex :: Key -> FilePath -> Maybe FileMode -> Annex LinkAnnexResult linkFromAnnex :: Key -> FilePath -> Maybe FileMode -> Annex LinkAnnexResult

View file

@ -383,10 +383,10 @@ removeDirect :: Key -> FilePath -> Annex ()
removeDirect k f = do removeDirect k f = do
void $ removeAssociatedFileUnchecked k f void $ removeAssociatedFileUnchecked k f
unlessM (inAnnex k) $ unlessM (inAnnex k) $
ifM (goodContent k f) -- If moveAnnex rejects the content of the key,
( moveAnnex k f -- treat that the same as its content having changed.
, logStatus k InfoMissing whenM (goodContent k f <&&> moveAnnex k f) $
) logStatus k InfoMissing
liftIO $ do liftIO $ do
nukeFile f nukeFile f
void $ tryIO $ removeDirectory $ parentDir f void $ tryIO $ removeDirectory $ parentDir f

View file

@ -172,10 +172,13 @@ ingest' preferredbackend (Just (LockedDown cfg source)) mk = withTSDelta $ \delt
go _ _ _ = failure "failed to generate a key" go _ _ _ = failure "failed to generate a key"
golocked key mcache s = do golocked key mcache s = do
catchNonAsync (moveAnnex key $ contentLocation source) v <- tryNonAsync (moveAnnex key $ contentLocation source)
(restoreFile (keyFilename source) key) case v of
Right True -> do
populateAssociatedFiles key source populateAssociatedFiles key source
success key mcache s success key mcache s
Right False -> giveup "failed to add content to annex"
Left e -> restoreFile (keyFilename source) key e
gounlocked key (Just cache) s = do gounlocked key (Just cache) s = do
-- Remove temp directory hard link first because -- Remove temp directory hard link first because
@ -352,8 +355,11 @@ cachedCurrentBranch = maybe cache (return . Just)
{- Adds a file to the work tree for the key, and stages it in the index. {- Adds a file to the work tree for the key, and stages it in the index.
- The content of the key may be provided in a temp file, which will be - The content of the key may be provided in a temp file, which will be
- moved into place. -} - moved into place.
addAnnexedFile :: FilePath -> Key -> Maybe FilePath -> Annex () -
- When the content of the key is not accepted into the annex, returns False.
-}
addAnnexedFile :: FilePath -> Key -> Maybe FilePath -> Annex Bool
addAnnexedFile file key mtmp = ifM (addUnlocked <&&> not <$> isDirect) addAnnexedFile file key mtmp = ifM (addUnlocked <&&> not <$> isDirect)
( do ( do
mode <- maybe mode <- maybe
@ -363,12 +369,13 @@ addAnnexedFile file key mtmp = ifM (addUnlocked <&&> not <$> isDirect)
stagePointerFile file mode =<< hashPointerFile key stagePointerFile file mode =<< hashPointerFile key
Database.Keys.addAssociatedFile key =<< inRepo (toTopFilePath file) Database.Keys.addAssociatedFile key =<< inRepo (toTopFilePath file)
case mtmp of case mtmp of
Just tmp -> do Just tmp -> ifM (moveAnnex key tmp)
moveAnnex key tmp ( linkunlocked mode >> return True
linkunlocked mode , writepointer mode >> return False
)
Nothing -> ifM (inAnnex key) Nothing -> ifM (inAnnex key)
( linkunlocked mode ( linkunlocked mode >> return True
, liftIO $ writePointerFile file key mode , writepointer mode >> return True
) )
, do , do
addLink file key Nothing addLink file key Nothing
@ -381,7 +388,7 @@ addAnnexedFile file key mtmp = ifM (addUnlocked <&&> not <$> isDirect)
whenM isDirect $ whenM isDirect $
Annex.Queue.flush Annex.Queue.flush
moveAnnex key tmp moveAnnex key tmp
Nothing -> return () Nothing -> return True
) )
where where
linkunlocked mode = do linkunlocked mode = do
@ -390,3 +397,4 @@ addAnnexedFile file key mtmp = ifM (addUnlocked <&&> not <$> isDirect)
LinkAnnexFailed -> liftIO $ LinkAnnexFailed -> liftIO $
writePointerFile file key mode writePointerFile file key mode
_ -> return () _ -> return ()
writepointer mode = liftIO $ writePointerFile file key mode

View file

@ -1,5 +1,10 @@
git-annex (6.20170215) UNRELEASED; urgency=medium git-annex (6.20170215) UNRELEASED; urgency=medium
* Cryptographically secure hashes can be forced to be used in a
repository, by setting annex.securehashesonly.
This does not prevent the git repository from containing files
with insecure hashes, but it does prevent the content of such files
from being added to .git/annex/objects.
* sync, merge: Fail when the current branch has no commits yet, instead * sync, merge: Fail when the current branch has no commits yet, instead
of not merging in anything from remotes and appearing to succeed. of not merging in anything from remotes and appearing to succeed.
* Run ssh with -n whenever input is not being piped into it, * Run ssh with -n whenever input is not being piped into it,

View file

@ -356,10 +356,13 @@ cleanup u url file key mtmp = case mtmp of
where where
go = do go = do
maybeShowJSON $ JSONChunk [("key", key2file key)] maybeShowJSON $ JSONChunk [("key", key2file key)]
setUrlPresent u key url
ifM (addAnnexedFile file key mtmp)
( do
when (isJust mtmp) $ when (isJust mtmp) $
logStatus key InfoPresent logStatus key InfoPresent
setUrlPresent u key url , liftIO $ maybe noop nukeFile mtmp
addAnnexedFile file key mtmp )
nodownload :: URLString -> Url.UrlInfo -> FilePath -> Annex (Maybe Key) nodownload :: URLString -> Url.UrlInfo -> FilePath -> Annex (Maybe Key)
nodownload url urlinfo file nodownload url urlinfo file

View file

@ -86,16 +86,16 @@ perform = do
whenM (liftIO $ not . isSymbolicLink <$> getSymbolicLinkStatus f) $ do whenM (liftIO $ not . isSymbolicLink <$> getSymbolicLinkStatus f) $ do
v <- tryNonAsync (moveAnnex k f) v <- tryNonAsync (moveAnnex k f)
case v of case v of
Right _ -> do Right True -> do
l <- calcRepo $ gitAnnexLink f k l <- calcRepo $ gitAnnexLink f k
liftIO $ createSymbolicLink l f liftIO $ createSymbolicLink l f
Left e -> catchNonAsync (restoreFile f k e) Right False -> warnlocked "Failed to move file to annex"
warnlocked Left e -> catchNonAsync (restoreFile f k e) $
warnlocked . show
showEndOk showEndOk
warnlocked :: SomeException -> Annex () warnlocked msg = do
warnlocked e = do warning msg
warning $ show e
warning "leaving this file as-is; correct this problem and run git annex add on it" warning "leaving this file as-is; correct this problem and run git annex add on it"
cleanup :: CommandCleanup cleanup :: CommandCleanup

View file

@ -74,9 +74,8 @@ perform src key = ifM move
, error "failed" , error "failed"
) )
where where
move = checkDiskSpaceToGet key False $ do move = checkDiskSpaceToGet key False $
moveAnnex key src moveAnnex key src
return True
cleanup :: Key -> CommandCleanup cleanup :: Key -> CommandCleanup
cleanup key = do cleanup key = do

View file

@ -82,6 +82,7 @@ data GitConfig = GitConfig
, annexPidLock :: Bool , annexPidLock :: Bool
, annexPidLockTimeout :: Seconds , annexPidLockTimeout :: Seconds
, annexAddUnlocked :: Bool , annexAddUnlocked :: Bool
, annexSecureHashesOnly :: Bool
, coreSymlinks :: Bool , coreSymlinks :: Bool
, coreSharedRepository :: SharedRepository , coreSharedRepository :: SharedRepository
, receiveDenyCurrentBranch :: DenyCurrentBranch , receiveDenyCurrentBranch :: DenyCurrentBranch
@ -136,6 +137,7 @@ extractGitConfig r = GitConfig
, annexPidLockTimeout = Seconds $ fromMaybe 300 $ , annexPidLockTimeout = Seconds $ fromMaybe 300 $
getmayberead (annex "pidlocktimeout") getmayberead (annex "pidlocktimeout")
, annexAddUnlocked = getbool (annex "addunlocked") False , annexAddUnlocked = getbool (annex "addunlocked") False
, annexSecureHashesOnly = getbool (annex "securehashesonly") False
, coreSymlinks = getbool "core.symlinks" True , coreSymlinks = getbool "core.symlinks" True
, coreSharedRepository = getSharedRepository r , coreSharedRepository = getSharedRepository r
, receiveDenyCurrentBranch = getDenyCurrentBranch r , receiveDenyCurrentBranch = getDenyCurrentBranch r