# Copyright 2005, 2006 Benoit Boissinot <benoit.boissinot@ens-lyon.org>## This software may be used and distributed according to the terms of the# GNU General Public License version 2 or any later version.'''commands to sign and verify changesets'''importos,tempfile,binasciifrommercurialimportutil,commands,matchfrommercurialimportnodeashgnodefrommercurial.i18nimport_classgpg(object):def__init__(self,path,key=None):self.path=pathself.key=(keyand" --local-user \"%s\""%key)or""defsign(self,data):gpgcmd="%s --sign --detach-sign%s"%(self.path,self.key)returnutil.filter(data,gpgcmd)defverify(self,data,sig):""" returns of the good and bad signatures"""sigfile=datafile=Nonetry:# create temporary filesfd,sigfile=tempfile.mkstemp(prefix="hg-gpg-",suffix=".sig")fp=os.fdopen(fd,'wb')fp.write(sig)fp.close()fd,datafile=tempfile.mkstemp(prefix="hg-gpg-",suffix=".txt")fp=os.fdopen(fd,'wb')fp.write(data)fp.close()gpgcmd=("%s --logger-fd 1 --status-fd 1 --verify ""\"%s\"\"%s\""%(self.path,sigfile,datafile))ret=util.filter("",gpgcmd)finally:forfin(sigfile,datafile):try:iff:os.unlink(f)except:passkeys=[]key,fingerprint=None,Noneerr=""forlinret.splitlines():# see DETAILS in the gnupg documentation# filter the logger outputifnotl.startswith("[GNUPG:]"):continuel=l[9:]ifl.startswith("ERRSIG"):err=_("error while verifying signature")breakelifl.startswith("VALIDSIG"):# fingerprint of the primary keyfingerprint=l.split()[10]elif(l.startswith("GOODSIG")orl.startswith("EXPSIG")orl.startswith("EXPKEYSIG")orl.startswith("BADSIG")):ifkeyisnotNone:keys.append(key+[fingerprint])key=l.split(" ",2)fingerprint=Noneiferr:returnerr,[]ifkeyisnotNone:keys.append(key+[fingerprint])returnerr,keysdefnewgpg(ui,**opts):"""create a new gpg instance"""gpgpath=ui.config("gpg","cmd","gpg")gpgkey=opts.get('key')ifnotgpgkey:gpgkey=ui.config("gpg","key",None)returngpg(gpgpath,gpgkey)defsigwalk(repo):""" walk over every sigs, yields a couple ((node, version, sig), (filename, linenumber)) """defparsefile(fileiter,context):ln=1forlinfileiter:ifnotl:continueyield(l.split(" ",2),(context,ln))ln+=1# read the headsfl=repo.file(".hgsigs")forrinreversed(fl.heads()):fn=".hgsigs|%s"%hgnode.short(r)foriteminparsefile(fl.read(r).splitlines(),fn):yielditemtry:# read local signaturesfn="localsigs"foriteminparsefile(repo.opener(fn),fn):yielditemexceptIOError:passdefgetkeys(ui,repo,mygpg,sigdata,context):"""get the keys who signed a data"""fn,ln=contextnode,version,sig=sigdataprefix="%s:%d"%(fn,ln)node=hgnode.bin(node)data=node2txt(repo,node,version)sig=binascii.a2b_base64(sig)err,keys=mygpg.verify(data,sig)iferr:ui.warn("%s:%d%s\n"%(fn,ln,err))returnNonevalidkeys=[]# warn for expired key and/or sigsforkeyinkeys:ifkey[0]=="BADSIG":ui.write(_("%s Bad signature from \"%s\"\n")%(prefix,key[2]))continueifkey[0]=="EXPSIG":ui.write(_("%s Note: Signature has expired"" (signed by: \"%s\")\n")%(prefix,key[2]))elifkey[0]=="EXPKEYSIG":ui.write(_("%s Note: This key has expired"" (signed by: \"%s\")\n")%(prefix,key[2]))validkeys.append((key[1],key[2],key[3]))returnvalidkeysdefsigs(ui,repo):"""list signed changesets"""mygpg=newgpg(ui)revs={}fordata,contextinsigwalk(repo):node,version,sig=datafn,ln=contexttry:n=repo.lookup(node)exceptKeyError:ui.warn(_("%s:%d node does not exist\n")%(fn,ln))continuer=repo.changelog.rev(n)keys=getkeys(ui,repo,mygpg,data,context)ifnotkeys:continuerevs.setdefault(r,[])revs[r].extend(keys)forrevinsorted(revs,reverse=True):forkinrevs[rev]:r="%5d:%s"%(rev,hgnode.hex(repo.changelog.node(rev)))ui.write("%-30s%s\n"%(keystr(ui,k),r))defcheck(ui,repo,rev):"""verify all the signatures there may be for a particular revision"""mygpg=newgpg(ui)rev=repo.lookup(rev)hexrev=hgnode.hex(rev)keys=[]fordata,contextinsigwalk(repo):node,version,sig=dataifnode==hexrev:k=getkeys(ui,repo,mygpg,data,context)ifk:keys.extend(k)ifnotkeys:ui.write(_("No valid signature for %s\n")%hgnode.short(rev))return# print summaryui.write("%s is signed by:\n"%hgnode.short(rev))forkeyinkeys:ui.write(" %s\n"%keystr(ui,key))defkeystr(ui,key):"""associate a string to a key (username, comment)"""keyid,user,fingerprint=keycomment=ui.config("gpg",fingerprint,None)ifcomment:return"%s (%s)"%(user,comment)else:returnuserdefsign(ui,repo,*revs,**opts):"""add a signature for the current or given revision If no revision is given, the parent of the working directory is used, or tip if no revision is checked out. See 'hg help dates' for a list of formats valid for -d/--date. """mygpg=newgpg(ui,**opts)sigver="0"sigmessage=""date=opts.get('date')ifdate:opts['date']=util.parsedate(date)ifrevs:nodes=[repo.lookup(n)forninrevs]else:nodes=[nodefornodeinrepo.dirstate.parents()ifnode!=hgnode.nullid]iflen(nodes)>1:raiseutil.Abort(_('uncommitted merge - please provide a ''specific revision'))ifnotnodes:nodes=[repo.changelog.tip()]forninnodes:hexnode=hgnode.hex(n)ui.write(_("Signing %d:%s\n")%(repo.changelog.rev(n),hgnode.short(n)))# build datadata=node2txt(repo,n,sigver)sig=mygpg.sign(data)ifnotsig:raiseutil.Abort(_("Error while signing"))sig=binascii.b2a_base64(sig)sig=sig.replace("\n","")sigmessage+="%s%s%s\n"%(hexnode,sigver,sig)# write itifopts['local']:repo.opener("localsigs","ab").write(sigmessage)returnmsigs=match.exact(repo.root,'',['.hgsigs'])s=repo.status(match=msigs,unknown=True,ignored=True)[:6]ifutil.any(s)andnotopts["force"]:raiseutil.Abort(_("working copy of .hgsigs is changed ""(please commit .hgsigs manually ""or use --force)"))repo.wfile(".hgsigs","ab").write(sigmessage)if'.hgsigs'notinrepo.dirstate:repo.add([".hgsigs"])ifopts["no_commit"]:returnmessage=opts['message']ifnotmessage:# we don't translate commit messagesmessage="\n".join(["Added signature for changeset %s"%hgnode.short(n)forninnodes])try:repo.commit(message,opts['user'],opts['date'],match=msigs)exceptValueError,inst:raiseutil.Abort(str(inst))defnode2txt(repo,node,ver):"""map a manifest into some text"""ifver=="0":return"%s\n"%hgnode.hex(node)else:raiseutil.Abort(_("unknown signature version"))cmdtable={"sign":(sign,[('l','local',None,_('make the signature local')),('f','force',None,_('sign even if the sigfile is modified')),('','no-commit',None,_('do not commit the sigfile after signing')),('k','key','',_('the key id to sign with')),('m','message','',_('commit message')),]+commands.commitopts2,_('hg sign [OPTION]... [REVISION]...')),"sigcheck":(check,[],_('hg sigcheck REVISION')),"sigs":(sigs,[],_('hg sigs')),}