From a4cc9211d61856eada799a72498eccd68bc50d91 Mon Sep 17 00:00:00 2001 From: Roman Lebedev Date: Mon, 29 Dec 2025 23:15:40 +0300 Subject: [PATCH] `InRelease` file: support `Signed-By` field https://wiki.debian.org/DebianRepository/Format#Signed-By says: > **Signed-By** > An optional field containing a comma separated list of > OpenPGP key fingerprints to be used for validating > the next Release file. The fingerprints must consist > only of hex digits and may not contain spaces. > The fingerprint specifies either the key the Release file > must be signed with or the key the signature key must be > a subkey of. The later match can be disabled by appending > an exclamation mark to the fingerprint. > > If the field is present, a client should only accept future updates > to the repository that are signed with keys listed in the field. > The field should be ignored if the Valid-Until field is not present > or if it is expired. For both the CLI tools and JSON, the field is taken as a string verbatim. When specified, we must also provide `Valid-Until` field, and i'm not sure there is an 'infinity' value for it, so 100 years will have to do? Fixes https://github.com/aptly-dev/aptly/issues/1497 --- AUTHORS | 1 + api/publish.go | 18 + cmd/publish_repo.go | 1 + cmd/publish_snapshot.go | 5 + cmd/publish_switch.go | 5 + cmd/publish_update.go | 5 + deb/format.go | 3 + deb/publish.go | 22 +- man/aptly.1 | 16 + system/t06_publish/PublishList5Test_gold | 4 + system/t06_publish/PublishRepo35Test_gold | 14 + system/t06_publish/PublishRepo35Test_release | 12 + system/t06_publish/PublishShow3Test_gold | 1 + system/t06_publish/PublishShow4Test_gold | 1 + system/t06_publish/PublishSnapshot43Test_gold | 13 + .../t06_publish/PublishSnapshot43Test_release | 12 + system/t06_publish/PublishSwitch17Test_gold | 9 + .../t06_publish/PublishSwitch17Test_release | 12 + system/t06_publish/PublishUpdate19Test_gold | 9 + .../t06_publish/PublishUpdate19Test_release | 12 + system/t06_publish/repo.py | 21 +- system/t06_publish/snapshot.py | 22 +- system/t06_publish/switch.py | 22 +- system/t06_publish/update.py | 21 +- system/t12_api/publish.py | 313 ++++++++++++++++++ 25 files changed, 569 insertions(+), 5 deletions(-) create mode 100644 system/t06_publish/PublishRepo35Test_gold create mode 100644 system/t06_publish/PublishRepo35Test_release create mode 100644 system/t06_publish/PublishSnapshot43Test_gold create mode 100644 system/t06_publish/PublishSnapshot43Test_release create mode 100644 system/t06_publish/PublishSwitch17Test_gold create mode 100644 system/t06_publish/PublishSwitch17Test_release create mode 100644 system/t06_publish/PublishUpdate19Test_gold create mode 100644 system/t06_publish/PublishUpdate19Test_release diff --git a/AUTHORS b/AUTHORS index d3f849278a..0014b68c80 100644 --- a/AUTHORS +++ b/AUTHORS @@ -77,3 +77,4 @@ List of contributors, in chronological order: * Yaksh Bariya (https://github.com/thunder-coding) * Juan Calderon-Perez (https://github.com/gaby) * Ato Araki (https://github.com/atotto) +* Roman Lebedev (https://github.com/LebedevRI) diff --git a/api/publish.go b/api/publish.go index 633fdc34c9..5c8d06adec 100644 --- a/api/publish.go +++ b/api/publish.go @@ -168,6 +168,8 @@ type publishedRepoCreateParams struct { SkipBz2 *bool ` json:"SkipBz2" example:"false"` // Provide index files by hash AcquireByHash *bool ` json:"AcquireByHash" example:"false"` + // An optional field containing a comma separated list of OpenPGP key fingerprints to be used for validating the next Release file. + SignedBy *string ` json:"SignedBy" example:""` // Enable multiple packages with the same filename in different distributions MultiDist *bool ` json:"MultiDist" example:"false"` } @@ -341,6 +343,10 @@ func apiPublishRepoOrSnapshot(c *gin.Context) { published.AcquireByHash = *b.AcquireByHash } + if b.SignedBy != nil { + published.SignedBy = *b.SignedBy + } + duplicate := collection.CheckDuplicate(published) if duplicate != nil { _ = collectionFactory.PublishedRepoCollection().LoadComplete(duplicate, collectionFactory) @@ -376,6 +382,8 @@ type publishedRepoUpdateSwitchParams struct { Snapshots []sourceParams ` json:"Snapshots"` // Provide index files by hash AcquireByHash *bool ` json:"AcquireByHash" example:"false"` + // An optional field containing a comma separated list of OpenPGP key fingerprints to be used for validating the next Release file + SignedBy *string ` json:"SignedBy" example:""` // Enable multiple packages with the same filename in different distributions MultiDist *bool ` json:"MultiDist" example:"false"` } @@ -461,6 +469,10 @@ func apiPublishUpdateSwitch(c *gin.Context) { published.AcquireByHash = *b.AcquireByHash } + if b.SignedBy != nil { + published.SignedBy = *b.SignedBy + } + if b.MultiDist != nil { published.MultiDist = *b.MultiDist } @@ -954,6 +966,8 @@ type publishedRepoUpdateParams struct { SkipCleanup *bool ` json:"SkipCleanup" example:"false"` // Provide index files by hash AcquireByHash *bool ` json:"AcquireByHash" example:"false"` + // An optional field containing a comma separated list of OpenPGP key fingerprints to be used for validating the next Release file + SignedBy *string ` json:"SignedBy" example:""` // Enable multiple packages with the same filename in different distributions MultiDist *bool ` json:"MultiDist" example:"false"` } @@ -1020,6 +1034,10 @@ func apiPublishUpdate(c *gin.Context) { published.AcquireByHash = *b.AcquireByHash } + if b.SignedBy != nil { + published.SignedBy = *b.SignedBy + } + if b.MultiDist != nil { published.MultiDist = *b.MultiDist } diff --git a/cmd/publish_repo.go b/cmd/publish_repo.go index 919710226d..a254cfaf72 100644 --- a/cmd/publish_repo.go +++ b/cmd/publish_repo.go @@ -51,6 +51,7 @@ Example: cmd.Flag.String("codename", "", "codename to publish (defaults to distribution)") cmd.Flag.Bool("force-overwrite", false, "overwrite files in package pool in case of mismatch") cmd.Flag.Bool("acquire-by-hash", false, "provide index files by hash") + cmd.Flag.String("signed-by", "", "an optional field containing a comma separated list of OpenPGP key fingerprints to be used for validating the next Release file") cmd.Flag.Bool("multi-dist", false, "enable multiple packages with the same filename in different distributions") return cmd diff --git a/cmd/publish_snapshot.go b/cmd/publish_snapshot.go index 7e0d8452ea..fbdf559e40 100644 --- a/cmd/publish_snapshot.go +++ b/cmd/publish_snapshot.go @@ -150,6 +150,10 @@ func aptlyPublishSnapshotOrRepo(cmd *commander.Command, args []string) error { published.AcquireByHash = context.Flags().Lookup("acquire-by-hash").Value.Get().(bool) } + if context.Flags().IsSet("signed-by") { + published.SignedBy = context.Flags().Lookup("signed-by").Value.String() + } + if context.Flags().IsSet("multi-dist") { published.MultiDist = context.Flags().Lookup("multi-dist").Value.Get().(bool) } @@ -247,6 +251,7 @@ Example: cmd.Flag.String("codename", "", "codename to publish (defaults to distribution)") cmd.Flag.Bool("force-overwrite", false, "overwrite files in package pool in case of mismatch") cmd.Flag.Bool("acquire-by-hash", false, "provide index files by hash") + cmd.Flag.String("signed-by", "", "an optional field containing a comma separated list of OpenPGP key fingerprints to be used for validating the next Release file") cmd.Flag.Bool("multi-dist", false, "enable multiple packages with the same filename in different distributions") return cmd diff --git a/cmd/publish_switch.go b/cmd/publish_switch.go index f39269a16d..6ae8531e95 100644 --- a/cmd/publish_switch.go +++ b/cmd/publish_switch.go @@ -99,6 +99,10 @@ func aptlyPublishSwitch(cmd *commander.Command, args []string) error { published.SkipBz2 = context.Flags().Lookup("skip-bz2").Value.Get().(bool) } + if context.Flags().IsSet("signed-by") { + published.SignedBy = context.Flags().Lookup("signed-by").Value.String() + } + if context.Flags().IsSet("multi-dist") { published.MultiDist = context.Flags().Lookup("multi-dist").Value.Get().(bool) } @@ -162,6 +166,7 @@ This command would switch published repository (with one component) named ppa/wh cmd.Flag.Bool("skip-bz2", false, "don't generate bzipped indexes") cmd.Flag.String("component", "", "component names to update (for multi-component publishing, separate components with commas)") cmd.Flag.Bool("force-overwrite", false, "overwrite files in package pool in case of mismatch") + cmd.Flag.String("signed-by", "", "an optional field containing a comma separated list of OpenPGP key fingerprints to be used for validating the next Release file") cmd.Flag.Bool("skip-cleanup", false, "don't remove unreferenced files in prefix/component") cmd.Flag.Bool("multi-dist", false, "enable multiple packages with the same filename in different distributions") diff --git a/cmd/publish_update.go b/cmd/publish_update.go index 6ea638d41f..34075bb2de 100644 --- a/cmd/publish_update.go +++ b/cmd/publish_update.go @@ -60,6 +60,10 @@ func aptlyPublishUpdate(cmd *commander.Command, args []string) error { published.SkipBz2 = context.Flags().Lookup("skip-bz2").Value.Get().(bool) } + if context.Flags().IsSet("signed-by") { + published.SignedBy = context.Flags().Lookup("signed-by").Value.String() + } + if context.Flags().IsSet("multi-dist") { published.MultiDist = context.Flags().Lookup("multi-dist").Value.Get().(bool) } @@ -125,6 +129,7 @@ Example: cmd.Flag.Bool("skip-contents", false, "don't generate Contents indexes") cmd.Flag.Bool("skip-bz2", false, "don't generate bzipped indexes") cmd.Flag.Bool("force-overwrite", false, "overwrite files in package pool in case of mismatch") + cmd.Flag.String("signed-by", "", "an optional field containing a comma separated list of OpenPGP key fingerprints to be used for validating the next Release file") cmd.Flag.Bool("skip-cleanup", false, "don't remove unreferenced files in prefix/component") cmd.Flag.Bool("multi-dist", false, "enable multiple packages with the same filename in different distributions") diff --git a/deb/format.go b/deb/format.go index 95febe35c5..7551dcbe4b 100644 --- a/deb/format.go +++ b/deb/format.go @@ -26,8 +26,11 @@ var ( "Version", "Codename", "Date", + "Valid-Until", "NotAutomatic", "ButAutomaticUpgrades", + "Acquire-By-Hash", + "Signed-By", "Architectures", "Architecture", "Components", diff --git a/deb/publish.go b/deb/publish.go index 46c9557f28..136809fefe 100644 --- a/deb/publish.go +++ b/deb/publish.go @@ -81,6 +81,11 @@ type PublishedRepo struct { // Provide index files per hash also AcquireByHash bool + // An optional field containing a comma separated list + // of OpenPGP key fingerprints to be used + // for validating the next Release file + SignedBy string + // Support multiple distributions MultiDist bool @@ -529,6 +534,7 @@ func (p *PublishedRepo) MarshalJSON() ([]byte, error) { "Storage": p.Storage, "SkipContents": p.SkipContents, "AcquireByHash": p.AcquireByHash, + "SignedBy": p.SignedBy, "MultiDist": p.MultiDist, }) } @@ -1070,6 +1076,9 @@ func (p *PublishedRepo) Publish(packagePool aptly.PackagePool, publishedStorageP if p.AcquireByHash { release["Acquire-By-Hash"] = "yes" } + if p.SignedBy != "" { + release["Signed-By"] = p.SignedBy + } var bufWriter *bufio.Writer bufWriter, err = indexes.ReleaseIndex(component, arch, udeb).BufWriter() @@ -1126,11 +1135,22 @@ func (p *PublishedRepo) Publish(packagePool aptly.PackagePool, publishedStorageP release["Label"] = p.GetLabel() release["Suite"] = p.GetSuite() release["Codename"] = p.GetCodename() - release["Date"] = time.Now().UTC().Format("Mon, 2 Jan 2006 15:04:05 MST") + datetime_format := "Mon, 2 Jan 2006 15:04:05 MST" + date_now := time.Now().UTC() + release["Date"] = date_now.Format(datetime_format) release["Architectures"] = strings.Join(utils.StrSlicesSubstract(p.Architectures, []string{ArchitectureSource}), " ") if p.AcquireByHash { release["Acquire-By-Hash"] = "yes" } + if p.SignedBy != "" { + // "If the field is present, a client should only accept future updates + // to the repository that are signed with keys listed in the field. + // The field should be ignored if the Valid-Until field + // is not present or if it is expired." + release["Signed-By"] = p.SignedBy + // Let's use a century as a "forever" value. + release["Valid-Until"] = date_now.AddDate(100, 0, 0).Format(datetime_format) + } release["Description"] = " Generated by aptly\n" release["MD5Sum"] = "" release["SHA1"] = "" diff --git a/man/aptly.1 b/man/aptly.1 index f1ab1eee43..5bd1581eb2 100644 --- a/man/aptly.1 +++ b/man/aptly.1 @@ -1565,6 +1565,10 @@ $ aptly publish repo testing Options: . .TP +\-\fBsigned\-by\fR +set value for Signed-By field +. +.TP \-\fBacquire\-by\-hash\fR provide index files by hash . @@ -1706,6 +1710,10 @@ $ aptly publish snapshot wheezy\-main Options: . .TP +\-\fBsigned\-by\fR +set value for Signed-By field +. +.TP \-\fBacquire\-by\-hash\fR provide index files by hash . @@ -2065,6 +2073,10 @@ This command would switch published repository (with one component) named ppa/wh Options: . .TP +\-\fBsigned\-by\fR +set value for Signed-By field +. +.TP \-\fBbatch\fR run GPG with detached tty . @@ -2171,6 +2183,10 @@ $ aptly publish update wheezy ppa Options: . .TP +\-\fBsigned\-by\fR +set value for Signed-By field +. +.TP \-\fBbatch\fR run GPG with detached tty . diff --git a/system/t06_publish/PublishList5Test_gold b/system/t06_publish/PublishList5Test_gold index 58b5dc0572..2474d7ab06 100644 --- a/system/t06_publish/PublishList5Test_gold +++ b/system/t06_publish/PublishList5Test_gold @@ -14,6 +14,7 @@ "Origin": "LP-PPA-gladky-anton-gnuplot", "Path": "./maverick", "Prefix": ".", + "SignedBy": "", "SkipContents": false, "SourceKind": "snapshot", "Sources": [ @@ -39,6 +40,7 @@ "Origin": "", "Path": "ppa/smira/wheezy", "Prefix": "ppa/smira", + "SignedBy": "", "SkipContents": false, "SourceKind": "snapshot", "Sources": [ @@ -65,6 +67,7 @@ "Origin": "origin1", "Path": "ppa/tr1/maverick", "Prefix": "ppa/tr1", + "SignedBy": "", "SkipContents": false, "SourceKind": "snapshot", "Sources": [ @@ -91,6 +94,7 @@ "Origin": "", "Path": "ppa/tr2/maverick", "Prefix": "ppa/tr2", + "SignedBy": "", "SkipContents": false, "SourceKind": "snapshot", "Sources": [ diff --git a/system/t06_publish/PublishRepo35Test_gold b/system/t06_publish/PublishRepo35Test_gold new file mode 100644 index 0000000000..c44a11e053 --- /dev/null +++ b/system/t06_publish/PublishRepo35Test_gold @@ -0,0 +1,14 @@ +Loading packages... +Generating metadata files and linking package files... +Finalizing metadata files... +Signing file 'Release' with gpg, please enter your passphrase when prompted: +Clearsigning file 'Release' with gpg, please enter your passphrase when prompted: + +Local repo local-repo has been successfully published. +Please setup your webserver to serve directory '${HOME}/.aptly/public' with autoindexing. +Now you can add following line to apt sources: + deb http://your-server/ maverick contrib + deb-src http://your-server/ maverick contrib +Don't forget to add your GPG key to apt with apt-key. + +You can also use `aptly serve` to publish your repositories over HTTP quickly. diff --git a/system/t06_publish/PublishRepo35Test_release b/system/t06_publish/PublishRepo35Test_release new file mode 100644 index 0000000000..d5db347be1 --- /dev/null +++ b/system/t06_publish/PublishRepo35Test_release @@ -0,0 +1,12 @@ +Origin: . maverick +Label: label35 +Suite: maverick +Codename: maverick +Signed-By: comma,separated,string +Architectures: i386 +Components: contrib +Description: Generated by aptly +MD5Sum: +SHA1: +SHA256: +SHA512: diff --git a/system/t06_publish/PublishShow3Test_gold b/system/t06_publish/PublishShow3Test_gold index aefbff7c33..e4879a4755 100644 --- a/system/t06_publish/PublishShow3Test_gold +++ b/system/t06_publish/PublishShow3Test_gold @@ -13,6 +13,7 @@ "Origin": "LP-PPA-gladky-anton-gnuplot", "Path": "./maverick", "Prefix": ".", + "SignedBy": "", "SkipContents": false, "SourceKind": "snapshot", "Sources": [ diff --git a/system/t06_publish/PublishShow4Test_gold b/system/t06_publish/PublishShow4Test_gold index cf95faee64..a71b98af9e 100644 --- a/system/t06_publish/PublishShow4Test_gold +++ b/system/t06_publish/PublishShow4Test_gold @@ -13,6 +13,7 @@ "Origin": "LP-PPA-gladky-anton-gnuplot", "Path": "ppa/smira/maverick", "Prefix": "ppa/smira", + "SignedBy": "", "SkipContents": false, "SourceKind": "snapshot", "Sources": [ diff --git a/system/t06_publish/PublishSnapshot43Test_gold b/system/t06_publish/PublishSnapshot43Test_gold new file mode 100644 index 0000000000..8738ec92fa --- /dev/null +++ b/system/t06_publish/PublishSnapshot43Test_gold @@ -0,0 +1,13 @@ +Loading packages... +Generating metadata files and linking package files... +Finalizing metadata files... +Signing file 'Release' with gpg, please enter your passphrase when prompted: +Clearsigning file 'Release' with gpg, please enter your passphrase when prompted: + +Snapshot snap43 has been successfully published. +Please setup your webserver to serve directory '/home/runner/.aptly/public' with autoindexing. +Now you can add following line to apt sources: + deb http://your-server/ maverick main +Don't forget to add your GPG key to apt with apt-key. + +You can also use `aptly serve` to publish your repositories over HTTP quickly. diff --git a/system/t06_publish/PublishSnapshot43Test_release b/system/t06_publish/PublishSnapshot43Test_release new file mode 100644 index 0000000000..0896e692ad --- /dev/null +++ b/system/t06_publish/PublishSnapshot43Test_release @@ -0,0 +1,12 @@ +Origin: LP-PPA-gladky-anton-gnuplot +Label: . maverick +Suite: maverick +Codename: maverick +Signed-By: string,separated,by,commas +Architectures: amd64 i386 +Components: main +Description: Generated by aptly +MD5Sum: +SHA1: +SHA256: +SHA512: diff --git a/system/t06_publish/PublishSwitch17Test_gold b/system/t06_publish/PublishSwitch17Test_gold new file mode 100644 index 0000000000..8ab33a81d2 --- /dev/null +++ b/system/t06_publish/PublishSwitch17Test_gold @@ -0,0 +1,9 @@ +Loading packages... +Generating metadata files and linking package files... +Finalizing metadata files... +Signing file 'Release' with gpg, please enter your passphrase when prompted: +Clearsigning file 'Release' with gpg, please enter your passphrase when prompted: +Cleaning up published repository ./maverick... +Cleaning up component 'main'... + +Published snapshot repository ./maverick (origin: LP-PPA-gladky-anton-gnuplot) [amd64, i386] publishes {main: [snap2]: Snapshot from mirror [gnuplot-maverick]: http://ppa.launchpad.net/gladky-anton/gnuplot/ubuntu/ maverick} has been successfully switched to new source. diff --git a/system/t06_publish/PublishSwitch17Test_release b/system/t06_publish/PublishSwitch17Test_release new file mode 100644 index 0000000000..119dafce2e --- /dev/null +++ b/system/t06_publish/PublishSwitch17Test_release @@ -0,0 +1,12 @@ +Origin: LP-PPA-gladky-anton-gnuplot +Label: . maverick +Suite: maverick +Codename: maverick +Signed-By: a,string +Architectures: amd64 i386 +Components: main +Description: Generated by aptly +MD5Sum: +SHA1: +SHA256: +SHA512: diff --git a/system/t06_publish/PublishUpdate19Test_gold b/system/t06_publish/PublishUpdate19Test_gold new file mode 100644 index 0000000000..3be101c498 --- /dev/null +++ b/system/t06_publish/PublishUpdate19Test_gold @@ -0,0 +1,9 @@ +Loading packages... +Generating metadata files and linking package files... +Finalizing metadata files... +Signing file 'Release' with gpg, please enter your passphrase when prompted: +Clearsigning file 'Release' with gpg, please enter your passphrase when prompted: +Cleaning up published repository ./maverick... +Cleaning up component 'main'... + +Published local repository ./maverick [i386, source] publishes {main: [local-repo]} has been updated successfully. diff --git a/system/t06_publish/PublishUpdate19Test_release b/system/t06_publish/PublishUpdate19Test_release new file mode 100644 index 0000000000..a7e40616e8 --- /dev/null +++ b/system/t06_publish/PublishUpdate19Test_release @@ -0,0 +1,12 @@ +Origin: . maverick +Label: . maverick +Suite: maverick +Codename: maverick +Signed-By: some,string +Architectures: i386 +Components: main +Description: Generated by aptly +MD5Sum: +SHA1: +SHA256: +SHA512: diff --git a/system/t06_publish/repo.py b/system/t06_publish/repo.py index f882a42862..115b28495a 100644 --- a/system/t06_publish/repo.py +++ b/system/t06_publish/repo.py @@ -6,7 +6,7 @@ def strip_processor(output): - return "\n".join([l for l in output.split("\n") if not l.startswith(' ') and not l.startswith('Date:')]) + return "\n".join([l for l in output.split("\n") if not l.startswith(' ') and not l.startswith('Date:') and not l.startswith('Valid-Until:')]) class PublishRepo1Test(BaseTest): @@ -951,3 +951,22 @@ def check(self): if 'main/dep11/README' not in pathsSeen: raise Exception("README file not included in release file") + + +class PublishRepo35Test(BaseTest): + """ + publish repo: signed-by + """ + fixtureCmds = [ + "aptly repo create local-repo", + "aptly repo add local-repo ${files}", + ] + runCmd = "aptly publish repo -keyring=${files}/aptly.pub -secret-keyring=${files}/aptly.sec -distribution=maverick -component=contrib -label=label35 -signed-by=comma,separated,string local-repo" + gold_processor = BaseTest.expand_environ + + def check(self): + super(PublishRepo35Test, self).check() + + # verify contents except of sums + self.check_file_contents( + 'public/dists/maverick/Release', 'release', match_prepare=strip_processor) diff --git a/system/t06_publish/snapshot.py b/system/t06_publish/snapshot.py index fb877bb755..b35d6f3440 100644 --- a/system/t06_publish/snapshot.py +++ b/system/t06_publish/snapshot.py @@ -6,7 +6,7 @@ def strip_processor(output): - return "\n".join([l for l in output.split("\n") if not l.startswith(' ') and not l.startswith('Date:')]) + return "\n".join([l for l in output.split("\n") if not l.startswith(' ') and not l.startswith('Date:') and not l.startswith('Valid-Until:')]) def sorted_processor(output): @@ -1419,3 +1419,23 @@ def check(self): 'public/pool/main/g/gnuplot/gnuplot-doc_4.6.1-1~maverick2_all.deb') self.check_exists( 'public/pool/maverick/main/g/gnuplot/gnuplot-doc_4.6.1-1~maverick2_all.deb') + + +class PublishSnapshot43Test(BaseTest): + """ + publish snapshot: signed-by + """ + fixtureDB = True + fixturePool = True + fixtureCmds = [ + "aptly snapshot create snap43 from mirror gnuplot-maverick", + ] + sortOutput = True + runCmd = "aptly publish snapshot -keyring=${files}/aptly.pub -secret-keyring=${files}/aptly.sec -signed-by=string,separated,by,commas snap43" + gold_processor = BaseTest.expand_environ + + def check(self): + super(PublishSnapshot43Test, self).check() + + self.check_file_contents( + 'public/dists/maverick/Release', 'release', match_prepare=strip_processor) diff --git a/system/t06_publish/switch.py b/system/t06_publish/switch.py index 4243a97874..77b950e58e 100644 --- a/system/t06_publish/switch.py +++ b/system/t06_publish/switch.py @@ -5,7 +5,7 @@ def strip_processor(output): - return "\n".join([l for l in output.split("\n") if not l.startswith(' ') and not l.startswith('Date:')]) + return "\n".join([l for l in output.split("\n") if not l.startswith(' ') and not l.startswith('Date:') and not l.startswith('Valid-Until:')]) class PublishSwitch1Test(BaseTest): @@ -603,3 +603,23 @@ def check(self): self.check_exists('public/dists/bookworm/main/binary-amd64/Packages.gz') self.check_exists('public/pool/bookworm/main/g/gnuplot/gnuplot-x11_4.6.1-1~maverick2_amd64.deb') + + +class PublishSwitch17Test(BaseTest): + """ + publish switch: signed-by + """ + fixtureDB = True + fixturePool = True + fixtureCmds = [ + "aptly snapshot create snap1 from mirror gnuplot-maverick", + "aptly snapshot create snap2 from mirror gnuplot-maverick", + "aptly publish snapshot -keyring=${files}/aptly.pub -secret-keyring=${files}/aptly.sec -distribution=maverick snap1", + ] + runCmd = "aptly publish switch -keyring=${files}/aptly.pub -secret-keyring=${files}/aptly.sec -signed-by=a,string maverick snap2" + gold_processor = BaseTest.expand_environ + + def check(self): + super(PublishSwitch17Test, self).check() + + self.check_file_contents('public/dists/maverick/Release', 'release', match_prepare=strip_processor) diff --git a/system/t06_publish/update.py b/system/t06_publish/update.py index 3a24ec1696..f4a1752653 100644 --- a/system/t06_publish/update.py +++ b/system/t06_publish/update.py @@ -5,7 +5,7 @@ def strip_processor(output): - return "\n".join([l for l in output.split("\n") if not l.startswith(' ') and not l.startswith('Date:')]) + return "\n".join([l for l in output.split("\n") if not l.startswith(' ') and not l.startswith('Date:') and not l.startswith('Valid-Until:')]) class PublishUpdate1Test(BaseTest): @@ -606,3 +606,22 @@ def check(self): components = sorted(components.split(' ')) if ['other-test', 'test'] != components: raise Exception("value of 'Components' in release file is '%s' and does not match '%s'." % (' '.join(components), 'other-test test')) + + +class PublishUpdate19Test(BaseTest): + """ + publish update: signed-by + """ + fixtureCmds = [ + "aptly repo create local-repo", + "aptly repo add local-repo ${files}/", + "aptly publish repo -keyring=${files}/aptly.pub -secret-keyring=${files}/aptly.sec -distribution=maverick local-repo", + "aptly repo remove local-repo pyspi" + ] + runCmd = "aptly publish update -keyring=${files}/aptly.pub -secret-keyring=${files}/aptly.sec -signed-by=some,string maverick" + gold_processor = BaseTest.expand_environ + + def check(self): + super(PublishUpdate19Test, self).check() + + self.check_file_contents('public/dists/maverick/Release', 'release', match_prepare=strip_processor) diff --git a/system/t12_api/publish.py b/system/t12_api/publish.py index 827515796f..58ed244a30 100644 --- a/system/t12_api/publish.py +++ b/system/t12_api/publish.py @@ -51,6 +51,7 @@ def check(self): 'ButAutomaticUpgrades': '', 'Path': prefix + '/' + 'wheezy', 'Prefix': prefix, + 'SignedBy': '', 'SkipContents': False, 'MultiDist': False, 'SourceKind': 'local', @@ -96,6 +97,7 @@ def check(self): 'ButAutomaticUpgrades': '', 'Path': './' + distribution, 'Prefix': ".", + 'SignedBy': '', 'SkipContents': False, 'MultiDist': False, 'SourceKind': 'local', @@ -167,6 +169,7 @@ def check(self): 'ButAutomaticUpgrades': '', 'Path': prefix + '/' + 'bookworm', 'Prefix': prefix, + 'SignedBy': '', 'SkipContents': False, 'MultiDist': True, 'SourceKind': 'local', @@ -189,6 +192,62 @@ def check(self): "public/" + prefix + "/pool/bookworm/main/b/boost-defaults/libboost-program-options-dev_1.49.0.1_i386.deb") +class PublishAPITestRepoSignedBy(APITest): + """ + POST /publish/:prefix (local repos), GET /publish + """ + fixtureGpg = True + + def check(self): + repo_name = self.random_name() + self.check_equal(self.post( + "/api/repos", json={"Name": repo_name, "DefaultDistribution": "wheezy"}).status_code, 201) + + d = self.random_name() + self.check_equal(self.upload("/api/files/" + d, + "libboost-program-options-dev_1.49.0.1_i386.deb", "pyspi_0.6.1-1.3.dsc", + "pyspi_0.6.1-1.3.diff.gz", "pyspi_0.6.1.orig.tar.gz", + "pyspi-0.6.1-1.3.stripped.dsc").status_code, 200) + + task = self.post_task("/api/repos/" + repo_name + "/file/" + d) + self.check_task(task) + + # publishing under prefix, default distribution + prefix = self.random_name() + task = self.post_task( + "/api/publish/" + prefix, + json={ + "SourceKind": "local", + "Sources": [{"Name": repo_name}], + "Signing": DefaultSigningOptions, + "SignedBy": "just,a,string" + } + ) + self.check_task(task) + repo_expected = { + 'AcquireByHash': False, + 'Architectures': ['i386', 'source'], + 'Codename': '', + 'Distribution': 'wheezy', + 'Label': '', + 'Origin': '', + 'NotAutomatic': '', + 'ButAutomaticUpgrades': '', + 'Path': prefix + '/' + 'wheezy', + 'Prefix': prefix, + 'SignedBy': 'just,a,string', + 'SkipContents': False, + 'MultiDist': False, + 'SourceKind': 'local', + 'Sources': [{'Component': 'main', 'Name': repo_name}], + 'Storage': '', + 'Suite': ''} + + all_repos = self.get("/api/publish") + self.check_equal(all_repos.status_code, 200) + self.check_in(repo_expected, all_repos.json()) + + class PublishSnapshotAPITest(APITest): """ POST /publish/:prefix (snapshots), GET /publish @@ -244,6 +303,7 @@ def check(self): 'ButAutomaticUpgrades': 'yes', 'Path': prefix + '/' + 'squeeze', 'Prefix': prefix, + 'SignedBy': '', 'SkipContents': False, 'SourceKind': 'snapshot', 'Sources': [{'Component': 'main', 'Name': snapshot_name}], @@ -265,6 +325,74 @@ def check(self): "public/" + prefix + "/pool/main/b/boost-defaults/libboost-program-options-dev_1.49.0.1_i386.deb") +class PublishSnapshotAPITestSignedBy(APITest): + """ + POST /publish/:prefix (snapshots), GET /publish + """ + + def check(self): + repo_name = self.random_name() + snapshot_name = self.random_name() + self.check_equal( + self.post("/api/repos", json={"Name": repo_name}).status_code, 201) + + d = self.random_name() + self.check_equal(self.upload("/api/files/" + d, + "libboost-program-options-dev_1.49.0.1_i386.deb").status_code, 200) + + task = self.post_task("/api/repos/" + repo_name + "/file/" + d) + self.check_task(task) + + task = self.post_task("/api/repos/" + repo_name + '/snapshots', json={'Name': snapshot_name}) + self.check_task(task) + + prefix = self.random_name() + task = self.post_task( + "/api/publish/" + prefix, + json={ + "AcquireByHash": True, + "SourceKind": "snapshot", + "Sources": [{"Name": snapshot_name}], + "Signing": DefaultSigningOptions, + "Distribution": "squeeze", + "NotAutomatic": "yes", + "ButAutomaticUpgrades": "yes", + "Origin": "earth", + "Label": "fun", + "SignedBy": "just,a,string", + } + ) + self.check_task(task) + + _id = task.json()['ID'] + resp = self.get("/api/tasks/" + str(_id) + "/detail") + self.check_equal(resp.json()['RemainingNumberOfPackages'], 0) + self.check_equal(resp.json()['TotalNumberOfPackages'], 1) + + repo_expected = { + 'AcquireByHash': True, + 'Architectures': ['i386'], + 'Codename': '', + 'Distribution': 'squeeze', + 'Label': 'fun', + 'Origin': 'earth', + 'MultiDist': False, + 'NotAutomatic': 'yes', + 'ButAutomaticUpgrades': 'yes', + 'Path': prefix + '/' + 'squeeze', + 'Prefix': prefix, + 'SignedBy': 'just,a,string', + 'SkipContents': False, + 'SourceKind': 'snapshot', + 'Sources': [{'Component': 'main', 'Name': snapshot_name}], + 'Storage': '', + 'Suite': '', + } + all_repos = self.get("/api/publish") + self.check_equal(all_repos.status_code, 200) + self.check_in(repo_expected, all_repos.json()) + + class PublishUpdateAPITestRepo(APITest): """ PUT /publish/:prefix/:distribution (local repos), DELETE /publish/:prefix/:distribution @@ -332,6 +460,7 @@ def check(self): 'ButAutomaticUpgrades': '', 'Path': prefix + '/' + 'wheezy', 'Prefix': prefix, + 'SignedBy': '', 'SkipContents': False, 'MultiDist': False, 'SourceKind': 'local', @@ -356,6 +485,86 @@ def check(self): self.check_not_exists("public/" + prefix + "dists/") +class PublishUpdateAPITestRepoSignedBy(APITest): + """ + PUT /publish/:prefix/:distribution (local repos), DELETE /publish/:prefix/:distribution + """ + fixtureGpg = True + + def check(self): + repo_name = self.random_name() + self.check_equal(self.post( + "/api/repos", json={"Name": repo_name, "DefaultDistribution": "wheezy"}).status_code, 201) + + d = self.random_name() + self.check_equal( + self.upload("/api/files/" + d, + "pyspi_0.6.1-1.3.dsc", + "pyspi_0.6.1-1.3.diff.gz", "pyspi_0.6.1.orig.tar.gz", + "pyspi-0.6.1-1.3.stripped.dsc").status_code, 200) + task = self.post_task("/api/repos/" + repo_name + "/file/" + d) + self.check_task(task) + + prefix = self.random_name() + task = self.post_task( + "/api/publish/" + prefix, + json={ + "Architectures": ["i386", "source"], + "SourceKind": "local", + "Sources": [{"Name": repo_name}], + "Signing": DefaultSigningOptions, + } + ) + self.check_task(task) + + self.check_not_exists( + "public/" + prefix + "/pool/main/b/boost-defaults/libboost-program-options-dev_1.49.0.1_i386.deb") + self.check_exists("public/" + prefix + + "/pool/main/p/pyspi/pyspi-0.6.1-1.3.stripped.dsc") + + d = self.random_name() + self.check_equal(self.upload("/api/files/" + d, + "libboost-program-options-dev_1.49.0.1_i386.deb").status_code, 200) + task = self.post_task("/api/repos/" + repo_name + "/file/" + d) + self.check_task(task) + + task = self.delete_task("/api/repos/" + repo_name + "/packages/", + json={"PackageRefs": ['Psource pyspi 0.6.1-1.4 f8f1daa806004e89']}) + self.check_task(task) + + # Update and specify SignedBy. + task = self.put_task( + "/api/publish/" + prefix + "/wheezy", + json={ + "Signing": DefaultSigningOptions, + "SignedBy": "just,a,string", + } + ) + self.check_task(task) + repo_expected = { + 'AcquireByHash': False, + 'Architectures': ['i386', 'source'], + 'Codename': '', + 'Distribution': 'wheezy', + 'Label': '', + 'Origin': '', + 'NotAutomatic': '', + 'ButAutomaticUpgrades': '', + 'Path': prefix + '/' + 'wheezy', + 'Prefix': prefix, + 'SignedBy': 'just,a,string', + 'SkipContents': False, + 'MultiDist': False, + 'SourceKind': 'local', + 'Sources': [{'Component': 'main', 'Name': repo_name}], + 'Storage': '', + 'Suite': ''} + + all_repos = self.get("/api/publish") + self.check_equal(all_repos.status_code, 200) + self.check_in(repo_expected, all_repos.json()) + + class PublishUpdateAPIMultiDist(APITest): """ Test MultiDist publishing to subdirectory @@ -423,6 +632,7 @@ def check(self): 'ButAutomaticUpgrades': '', 'Path': prefix + '/' + 'bookworm', 'Prefix': prefix, + 'SignedBy': '', 'SkipContents': False, 'MultiDist': True, 'SourceKind': 'local', @@ -524,6 +734,7 @@ def _do_update(result, index): 'ButAutomaticUpgrades': '', 'Path': prefix + '/' + 'wheezy', 'Prefix': prefix, + 'SignedBy': '', 'SkipContents': False, 'MultiDist': False, 'SourceKind': 'local', @@ -622,6 +833,7 @@ def check(self): 'ButAutomaticUpgrades': '', 'Path': prefix + '/' + 'wheezy', 'Prefix': prefix, + 'SignedBy': '', 'SkipContents': False, 'MultiDist': False, 'SourceKind': 'local', @@ -690,6 +902,7 @@ def check(self): 'Origin': '', 'Path': prefix + '/' + 'wheezy', 'Prefix': prefix, + 'SignedBy': '', 'SkipContents': False, 'MultiDist': False, 'SourceKind': 'snapshot', @@ -738,6 +951,7 @@ def check(self): 'ButAutomaticUpgrades': '', 'Path': prefix + '/' + 'wheezy', 'Prefix': prefix, + 'SignedBy': '', 'SkipContents': True, 'MultiDist': False, 'SourceKind': 'snapshot', @@ -759,6 +973,100 @@ def check(self): self.check_not_exists("public/" + prefix + "dists/") +class PublishSwitchAPITestRepoSignedBy(APITest): + """ + PUT /publish/:prefix/:distribution (snapshots), DELETE /publish/:prefix/:distribution + """ + fixtureGpg = True + + def check(self): + repo_name = self.random_name() + self.check_equal(self.post( + "/api/repos", json={"Name": repo_name, "DefaultDistribution": "wheezy"}).status_code, 201) + + d = self.random_name() + self.check_equal( + self.upload("/api/files/" + d, + "pyspi_0.6.1-1.3.dsc", + "pyspi_0.6.1-1.3.diff.gz", "pyspi_0.6.1.orig.tar.gz", + "pyspi-0.6.1-1.3.stripped.dsc").status_code, 200) + task = self.post_task("/api/repos/" + repo_name + "/file/" + d) + self.check_task(task) + + snapshot1_name = self.random_name() + task = self.post_task("/api/repos/" + repo_name + '/snapshots', json={'Name': snapshot1_name}) + self.check_task(task) + + prefix = self.random_name() + task = self.post_task( + "/api/publish/" + prefix, + json={ + "Architectures": ["i386", "source"], + "SourceKind": "snapshot", + "Sources": [{"Name": snapshot1_name}], + "Signing": DefaultSigningOptions, + }) + self.check_task(task) + + repo_expected = { + 'AcquireByHash': False, + 'Architectures': ['i386', 'source'], + 'Codename': '', + 'Distribution': 'wheezy', + 'Label': '', + 'NotAutomatic': '', + 'ButAutomaticUpgrades': '', + 'Origin': '', + 'Path': prefix + '/' + 'wheezy', + 'Prefix': prefix, + 'SignedBy': '', + 'SkipContents': False, + 'MultiDist': False, + 'SourceKind': 'snapshot', + 'Sources': [{'Component': 'main', 'Name': snapshot1_name}], + 'Storage': '', + 'Suite': ''} + all_repos = self.get("/api/publish") + self.check_equal(all_repos.status_code, 200) + self.check_in(repo_expected, all_repos.json()) + + snapshot2_name = self.random_name() + task = self.post_task("/api/repos/" + repo_name + '/snapshots', json={'Name': snapshot2_name}) + self.check_task(task) + + task = self.put_task( + "/api/publish/" + prefix + "/wheezy", + json={ + "Snapshots": [{"Component": "main", "Name": snapshot2_name}], + "Signing": DefaultSigningOptions, + "SkipContents": True, + "SignedBy": "just,a,string", + }) + self.check_task(task) + repo_expected = { + 'AcquireByHash': False, + 'Architectures': ['i386', 'source'], + 'Codename': '', + 'Distribution': 'wheezy', + 'Label': '', + 'Origin': '', + 'NotAutomatic': '', + 'ButAutomaticUpgrades': '', + 'Path': prefix + '/' + 'wheezy', + 'Prefix': prefix, + 'SignedBy': 'just,a,string', + 'SkipContents': True, + 'MultiDist': False, + 'SourceKind': 'snapshot', + 'Sources': [{'Component': 'main', 'Name': snapshot2_name}], + 'Storage': '', + 'Suite': ''} + + all_repos = self.get("/api/publish") + self.check_equal(all_repos.status_code, 200) + self.check_in(repo_expected, all_repos.json()) + + class PublishSwitchAPISkipCleanupTestRepo(APITest): """ PUT /publish/:prefix/:distribution (snapshots), DELETE /publish/:prefix/:distribution @@ -804,6 +1112,7 @@ def check(self): 'Origin': '', 'Path': prefix + '/' + 'wheezy', 'Prefix': prefix, + 'SignedBy': '', 'SkipContents': False, 'MultiDist': False, 'SourceKind': 'snapshot', @@ -842,6 +1151,7 @@ def check(self): 'Origin': '', 'Path': prefix + '/' + 'otherdist', 'Prefix': prefix, + 'SignedBy': '', 'SkipContents': False, 'MultiDist': False, 'SourceKind': 'snapshot', @@ -885,6 +1195,7 @@ def check(self): 'ButAutomaticUpgrades': '', 'Path': prefix + '/' + 'wheezy', 'Prefix': prefix, + 'SignedBy': '', 'SkipContents': True, 'MultiDist': False, 'SourceKind': 'snapshot', @@ -946,6 +1257,7 @@ def check(self): 'Origin': '', 'Path': prefix + '/' + 'wheezy', 'Prefix': prefix, + 'SignedBy': '', 'SkipContents': False, 'MultiDist': False, 'SourceKind': 'local', @@ -1547,6 +1859,7 @@ def check(self): 'ButAutomaticUpgrades': '', 'Path': prefix + '/' + 'wheezy', 'Prefix': prefix, + 'SignedBy': '', 'SkipContents': True, 'MultiDist': False, 'SourceKind': 'local',