Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

check signatures for download and git fetchers #305

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions HaikuPorter/Port.py
Original file line number Diff line number Diff line change
Expand Up @@ -639,6 +639,7 @@ def downloadSource(self):
for source in self.sources:
source.fetch(self)
source.validateChecksum(self)
source.validateFingerprint(self)

def unpackSource(self):
"""Unpack the source archive(s)"""
Expand Down Expand Up @@ -982,11 +983,17 @@ def _parseRecipeFile(self, showWarnings, forceAllowUnstable=False):
basedOnSourcePackage = False
## REFACTOR it looks like this method should be setup and dispatch

pgpkeys = keys['PGPKEYS'] if 'PGPKEYS' in keys else None
if not 'SOURCE_SIG_URI' in keys:
keys['SOURCE_SIG_URI'] = []

for index in sorted(list(keys['SOURCE_URI'].keys()),
key=cmp_to_key(naturalCompare)):
source = Source(self, index, keys['SOURCE_URI'][index],
keys['SOURCE_FILENAME'].get(index, None),
keys['CHECKSUM_SHA256'].get(index, None),
keys['SOURCE_SIG_URI'].get(index, None),
pgpkeys,
keys['SOURCE_DIR'].get(index, None),
keys['PATCHES'].get(index, []),
keys['ADDITIONAL_FILES'].get(index, []))
Expand Down
14 changes: 14 additions & 0 deletions HaikuPorter/RecipeAttributes.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,13 @@ def getRecipeFormatVersion():
'extendable': Extendable.NO,
'indexable': False,
},
'PGPKEYS': {
'type': list,
'required': False,
'default': {},
'extendable': Extendable.NO,
'indexable': False,
},

# indexable, i.e. per-source attributes
'ADDITIONAL_FILES': {
Expand Down Expand Up @@ -105,6 +112,13 @@ def getRecipeFormatVersion():
'extendable': Extendable.NO,
'indexable': True,
},
'SOURCE_SIG_URI': {
'type': list,
'required': False,
'default': {},
'extendable': Extendable.NO,
'indexable': True,
},

# extendable, i.e. per-package attributes
'ARCHITECTURES': {
Expand Down
42 changes: 37 additions & 5 deletions HaikuPorter/Source.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,14 @@
# -- A source archive (or checkout) -------------------------------------------

class Source(object):
def __init__(self, port, index, uris, fetchTargetName, checksum,
sourceDir, patches, additionalFiles):
def __init__(self, port, index, uris, fetchTargetName, checksum, sigUri,
pgpkeys, sourceDir, patches, additionalFiles):
self.index = index
self.uris = uris
self.fetchTargetName = fetchTargetName
self.checksum = checksum
self.sigUri = sigUri
self.pgpkeys = pgpkeys
self.patches = patches
self.additionalFiles = additionalFiles

Expand Down Expand Up @@ -130,7 +132,7 @@ def fetch(self, port):
(unusedType, baseUri, rev) = parseCheckoutUri(uri)
if baseUri == storedBaseUri:
self.sourceFetcher \
= createSourceFetcher(uri, self.fetchTarget)
= createSourceFetcher(uri, self.fetchTarget, self.sigUri)
if rev != storedRev:
self.sourceFetcher.updateToRev(rev)
storeStringInFile(uri, self.fetchTarget + '.uri')
Expand All @@ -144,7 +146,7 @@ def fetch(self, port):
warn("Stored SOURCE_URI is no longer in recipe, automatic "
u"repository update won't work")
self.sourceFetcher \
= createSourceFetcher(storedUri, self.fetchTarget)
= createSourceFetcher(storedUri, self.fetchTarget, self.sigUri)

return
else:
Expand All @@ -158,7 +160,7 @@ def fetch(self, port):
for uri in self.uris:
try:
info('\nDownloading: ' + uri + ' ...')
sourceFetcher = createSourceFetcher(uri, self.fetchTarget)
sourceFetcher = createSourceFetcher(uri, self.fetchTarget, self.sigUri)
sourceFetcher.fetch()

# ok, fetching the source was successful, we keep the source
Expand Down Expand Up @@ -271,6 +273,36 @@ def validateChecksum(self, port):

port.setFlag('validate', self.index)

def validateFingerprint(self, port):
"""Make sure that the fingerprint matches the expectations"""

if not self.sourceFetcher.sourceShouldBeVerified:
return

# Check to see if the source was already verified.
if port.checkFlag('verified', self.index) and not getOption('force'):
info('Skipping fingerprint validation of ' + self.fetchTargetName)
return

info('Validating fingerprint of ' + self.fetchTargetName)
hexdigest = fingerprint = self.sourceFetcher.findSignature()
if hexdigest is None:
sysExit('Found no fingerprint or no public key to match')

if self.pgpkeys is not None and len(self.pgpkeys) > 0:
for pgpkey in self.pgpkeys:
if hexdigest == pgpkey:
port.setFlag('verified', self.index)
return
sysExit('Found unexpected fingerprint: ' + hexdigest)
else:
warn('----- PGPKEYS TEMPLATE -----')
warn('PGPKEYS=(%(digest)s)' % {
"digest": hexdigest})
warn('-----------------------------')

port.setFlag('verified', self.index)

@property
def isFromSourcePackage(self):
"""Determines whether or not this source comes from a source package"""
Expand Down
88 changes: 85 additions & 3 deletions HaikuPorter/SourceFetcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ class SourceFetcherForBazaar(object):
def __init__(self, uri, fetchTarget):
self.fetchTarget = fetchTarget
self.sourceShouldBeValidated = False
self.sourceShouldBeVerified = False

(unusedType, self.uri, self.rev) = parseCheckoutUri(uri)

Expand Down Expand Up @@ -158,6 +159,7 @@ class SourceFetcherForCvs(object):
def __init__(self, uri, fetchTarget):
self.fetchTarget = fetchTarget
self.sourceShouldBeValidated = False
self.sourceShouldBeVerified = False

(unusedType, uri, self.rev) = parseCheckoutUri(uri)

Expand Down Expand Up @@ -199,10 +201,12 @@ def unpack(self, sourceBaseDir, sourceSubDir, foldSubDir):
# -- Fetches sources via wget -------------------------------------------------

class SourceFetcherForDownload(object):
def __init__(self, uri, fetchTarget):
def __init__(self, uri, fetchTarget, sigUri):
self.fetchTarget = fetchTarget
self.uri = uri
self.sigUri = sigUri
self.sourceShouldBeValidated = True
self.sourceShouldBeVerified = self.sigUri is not None

def fetch(self):
downloadDir = os.path.dirname(self.fetchTarget)
Expand Down Expand Up @@ -244,12 +248,52 @@ def unpack(self, sourceBaseDir, sourceSubDir, foldSubDir):
def calcChecksum(self):
return calcChecksumFile(self.fetchTarget)

def findSignature(self):
ensureCommandIsAvailable('wget')
ensureCommandIsAvailable('gpg')
downloadDir = os.path.dirname(self.fetchTarget)
sigFilename = self.sigUri[0]
sigFilename = sigFilename[sigFilename.rindex('/') + 1:]
filename = self.fetchTarget[self.fetchTarget.rindex('/') + 1:]
args = ['wget', '-c', '--tries=1', '--timeout=10', self.sigUri[0]]

code = 0
for tries in range(0, 3):
process = Popen(args, cwd=downloadDir, stdout=PIPE, stderr=STDOUT)
for line in iter(process.stdout.readline, b''):
info(line.decode('utf-8')[:-1])
process.stdout.close()
code = process.wait()
if code in (0, 2, 6, 8):
# 0: success
# 2: parse error of command line
# 6: auth failure
# 8: error response from server
break

time.sleep(3)

if code:
raise CalledProcessError(code, args)
command = 'gpg --verify --status-fd 1 %s %s 2>/dev/null' % (sigFilename, filename)
try:
output = check_output(command, shell=True, cwd=downloadDir).decode('utf-8')
except CalledProcessError as e:
return None
for line in output.split('\n'):
if 'VALIDSIG' in line:
print(line)
return line.split(' ')[11]
return None


# -- Fetches sources via fossil -----------------------------------------------

class SourceFetcherForFossil(object):
def __init__(self, uri, fetchTarget):
self.fetchTarget = fetchTarget
self.sourceShouldBeValidated = False
self.sourceShouldBeVerified = False

(unusedType, self.uri, self.rev) = parseCheckoutUri(uri)

Expand Down Expand Up @@ -286,13 +330,19 @@ class SourceFetcherForGit(object):
def __init__(self, uri, fetchTarget):
self.fetchTarget = fetchTarget
self.sourceShouldBeValidated = False
self.sourceShouldBeVerified = False
self.isCommit=False

(unusedType, self.uri, self.rev) = parseCheckoutUri(uri)
if not self.rev:
self.rev = 'HEAD'
if self.rev.startswith('tag=') or self.rev.startswith('commit='):
self.isCommit=self.rev.startswith('commit=')
self.rev=self.rev[self.rev.find('=') + 1:]
self.sourceShouldBeValidated = True
if self.uri.endswith('?signed'):
self.sourceShouldBeVerified = True
self.uri=self.uri[:-len('?signed')]

def fetch(self):
if not self.sourceShouldBeValidated:
Expand All @@ -317,6 +367,11 @@ def updateToRev(self, rev):
ensureCommandIsAvailable('git')

self.rev = rev
if self.rev.startswith('tag=') or self.rev.startswith('commit='):
self.isCommit=self.rev.startswith('commit=')
self.rev=self.rev[self.rev.find('=') + 1:]
self.sourceShouldBeValidated = True

command = 'git rev-list --max-count=1 %s &>/dev/null' % self.rev
try:
output = check_output(command, shell=True, cwd=self.fetchTarget).decode('utf-8')
Expand Down Expand Up @@ -358,13 +413,37 @@ def calcChecksum(self):
checksum = output[:output.find(' ')]
return checksum

def findSignature(self):
ensureCommandIsAvailable('git')
ensureCommandIsAvailable('gpg')
command = 'GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=/dev/null git '
if self.isCommit:
command += 'verify-commit'
else:
command += 'verify-tag'
command += ' --raw "%s" 2>&1' % (self.rev)
try:
output = check_output(command, shell=True, cwd=self.fetchTarget).decode('utf-8')
except CalledProcessError as e:
warn("COULDN'T FIND PUBLIC KEY")
for line in e.output.decode().split('\n'):
if "ERRSIG" in line:
key = line.split(' ')[8]
warn("IMPORT WITH: gpg --search-keys %s" % key)
return None
for line in output.split('\n'):
if 'VALIDSIG' in line:
return line.split(' ')[11]
return None

# -- Fetches sources from local disk ------------------------------------------

class SourceFetcherForLocalFile(object):
def __init__(self, uri, fetchTarget):
self.fetchTarget = fetchTarget
self.uri = uri
self.sourceShouldBeValidated = False
self.sourceShouldBeVerified = False

def fetch(self):
# just symlink the local file to fetchTarget (if it exists)
Expand All @@ -390,6 +469,7 @@ class SourceFetcherForMercurial(object):
def __init__(self, uri, fetchTarget):
self.fetchTarget = fetchTarget
self.sourceShouldBeValidated = False
self.sourceShouldBeVerified = False

(unusedType, self.uri, self.rev) = parseCheckoutUri(uri)

Expand Down Expand Up @@ -436,6 +516,7 @@ def __init__(self, uri, fetchTarget):
self.fetchTarget = fetchTarget
self.uri = uri
self.sourceShouldBeValidated = False
self.sourceShouldBeVerified = False
self.sourcePackagePath = self.uri[4:]

def fetch(self):
Expand Down Expand Up @@ -473,6 +554,7 @@ class SourceFetcherForSubversion(object):
def __init__(self, uri, fetchTarget):
self.fetchTarget = fetchTarget
self.sourceShouldBeValidated = False
self.sourceShouldBeVerified = False

(unusedType, self.uri, self.rev) = parseCheckoutUri(uri)

Expand All @@ -499,7 +581,7 @@ def unpack(self, sourceBaseDir, sourceSubDir, foldSubDir):

# -- source fetcher factory function for given URI ----------------------------

def createSourceFetcher(uri, fetchTarget):
def createSourceFetcher(uri, fetchTarget, sigUri):
"""Creates an appropriate source fetcher for the given URI"""

lowerUri = uri.lower()
Expand All @@ -514,7 +596,7 @@ def createSourceFetcher(uri, fetchTarget):
elif lowerUri.startswith('hg'):
return SourceFetcherForMercurial(uri, fetchTarget)
elif lowerUri.startswith('http') or lowerUri.startswith('ftp'):
return SourceFetcherForDownload(uri, fetchTarget)
return SourceFetcherForDownload(uri, fetchTarget, sigUri)
elif lowerUri.startswith('pkg:'):
return SourceFetcherForSourcePackage(uri, fetchTarget)
elif lowerUri.startswith('svn'):
Expand Down
Loading