From f946c1a8c0c9cbae5d3177698e9603971599957c Mon Sep 17 00:00:00 2001 From: Ales Bregar Date: Mon, 11 Aug 2025 10:34:15 +0200 Subject: [PATCH 01/12] - #309 adding gpgKeys config key, accepting array of keyRef, cli args has precedence - #691 adding handling of multiple keyRefs when signing with gpg --- cmd/publish.go | 38 +++++- cmd/publish_repo.go | 2 +- cmd/publish_snapshot.go | 2 +- cmd/publish_switch.go | 2 +- cmd/publish_update.go | 2 +- pgp/gnupg.go | 12 +- utils/config.go | 8 +- utils/config_test.go | 289 ++++++++++++++++++++-------------------- 8 files changed, 200 insertions(+), 155 deletions(-) diff --git a/cmd/publish.go b/cmd/publish.go index 4217ff87a6..70b65e34a4 100644 --- a/cmd/publish.go +++ b/cmd/publish.go @@ -1,6 +1,9 @@ package cmd import ( + "fmt" + "strings" + "github.com/aptly-dev/aptly/pgp" "github.com/smira/commander" "github.com/smira/flag" @@ -12,7 +15,23 @@ func getSigner(flags *flag.FlagSet) (pgp.Signer, error) { } signer := context.GetSigner() - signer.SetKey(flags.Lookup("gpg-key").Value.String()) + + var gpgKeys []string + + // CLI args have priority over config + cliKeys := flags.Lookup("gpg-key").Value.Get().([]string) + if len(cliKeys) > 0 { + gpgKeys = cliKeys + } else if len(context.Config().GpgKeys) > 0 { + gpgKeys = context.Config().GpgKeys + } + + if len(gpgKeys) > 0 { + fmt.Printf("Signing with following gpg keys %s\n", strings.Join(gpgKeys, ", ")) + } + for _, gpgKey := range gpgKeys { + signer.SetKey(gpgKey) + } signer.SetKeyRing(flags.Lookup("keyring").Value.String(), flags.Lookup("secret-keyring").Value.String()) signer.SetPassphrase(flags.Lookup("passphrase").Value.String(), flags.Lookup("passphrase-file").Value.String()) signer.SetBatch(flags.Lookup("batch").Value.Get().(bool)) @@ -26,6 +45,23 @@ func getSigner(flags *flag.FlagSet) (pgp.Signer, error) { } +type gpgKeyFlag struct { + gpgKeys []string +} + +func (k *gpgKeyFlag) Set(value string) error { + k.gpgKeys = append(k.gpgKeys, value) + return nil +} + +func (k *gpgKeyFlag) Get() interface{} { + return k.gpgKeys +} + +func (k *gpgKeyFlag) String() string { + return strings.Join(k.gpgKeys, ",") +} + func makeCmdPublish() *commander.Command { return &commander.Command{ UsageLine: "publish", diff --git a/cmd/publish_repo.go b/cmd/publish_repo.go index 919710226d..7e7111ba3a 100644 --- a/cmd/publish_repo.go +++ b/cmd/publish_repo.go @@ -34,7 +34,7 @@ Example: } cmd.Flag.String("distribution", "", "distribution name to publish") cmd.Flag.String("component", "", "component name to publish (for multi-component publishing, separate components with commas)") - cmd.Flag.String("gpg-key", "", "GPG key ID to use when signing the release") + cmd.Flag.Var(&gpgKeyFlag{}, "gpg-key", "GPG key ID to use when signing the release (repeatable, can be specified multiple times)") cmd.Flag.Var(&keyRingsFlag{}, "keyring", "GPG keyring to use (instead of default)") cmd.Flag.String("secret-keyring", "", "GPG secret keyring to use (instead of default)") cmd.Flag.String("passphrase", "", "GPG passphrase for the key (warning: could be insecure)") diff --git a/cmd/publish_snapshot.go b/cmd/publish_snapshot.go index 7e0d8452ea..6ca9d7a28d 100644 --- a/cmd/publish_snapshot.go +++ b/cmd/publish_snapshot.go @@ -230,7 +230,7 @@ Example: } cmd.Flag.String("distribution", "", "distribution name to publish") cmd.Flag.String("component", "", "component name to publish (for multi-component publishing, separate components with commas)") - cmd.Flag.String("gpg-key", "", "GPG key ID to use when signing the release") + cmd.Flag.Var(&gpgKeyFlag{}, "gpg-key", "GPG key ID to use when signing the release (repeatable, can be specified multiple times)") cmd.Flag.Var(&keyRingsFlag{}, "keyring", "GPG keyring to use (instead of default)") cmd.Flag.String("secret-keyring", "", "GPG secret keyring to use (instead of default)") cmd.Flag.String("passphrase", "", "GPG passphrase for the key (warning: could be insecure)") diff --git a/cmd/publish_switch.go b/cmd/publish_switch.go index f39269a16d..fe80036967 100644 --- a/cmd/publish_switch.go +++ b/cmd/publish_switch.go @@ -151,7 +151,7 @@ This command would switch published repository (with one component) named ppa/wh `, Flag: *flag.NewFlagSet("aptly-publish-switch", flag.ExitOnError), } - cmd.Flag.String("gpg-key", "", "GPG key ID to use when signing the release") + cmd.Flag.Var(&gpgKeyFlag{}, "gpg-key", "GPG key ID to use when signing the release (repeatable, can be specified multiple times)") cmd.Flag.Var(&keyRingsFlag{}, "keyring", "GPG keyring to use (instead of default)") cmd.Flag.String("secret-keyring", "", "GPG secret keyring to use (instead of default)") cmd.Flag.String("passphrase", "", "GPG passphrase for the key (warning: could be insecure)") diff --git a/cmd/publish_update.go b/cmd/publish_update.go index 6ea638d41f..d15c7ab46d 100644 --- a/cmd/publish_update.go +++ b/cmd/publish_update.go @@ -115,7 +115,7 @@ Example: `, Flag: *flag.NewFlagSet("aptly-publish-update", flag.ExitOnError), } - cmd.Flag.String("gpg-key", "", "GPG key ID to use when signing the release") + cmd.Flag.Var(&gpgKeyFlag{}, "gpg-key", "GPG key ID to use when signing the release (repeatable, can be specified multiple times)") cmd.Flag.Var(&keyRingsFlag{}, "keyring", "GPG keyring to use (instead of default)") cmd.Flag.String("secret-keyring", "", "GPG secret keyring to use (instead of default)") cmd.Flag.String("passphrase", "", "GPG passphrase for the key (warning: could be insecure)") diff --git a/pgp/gnupg.go b/pgp/gnupg.go index 3edf121024..dbff8ddcc1 100644 --- a/pgp/gnupg.go +++ b/pgp/gnupg.go @@ -22,7 +22,7 @@ var ( type GpgSigner struct { gpg string version GPGVersion - keyRef string + keyRefs []string keyring, secretKeyring string passphrase, passphraseFile string batch bool @@ -35,7 +35,11 @@ func (g *GpgSigner) SetBatch(batch bool) { // SetKey sets key ID to use when signing files func (g *GpgSigner) SetKey(keyRef string) { - g.keyRef = keyRef + if g.keyRefs == nil { + g.keyRefs = []string{strings.TrimSpace(keyRef)} + } else { + g.keyRefs = append(g.keyRefs, strings.TrimSpace(keyRef)) + } } // SetKeyRing allows to set custom keyring and secretkeyring @@ -57,8 +61,8 @@ func (g *GpgSigner) gpgArgs() []string { args = append(args, "--secret-keyring", g.secretKeyring) } - if g.keyRef != "" { - args = append(args, "-u", g.keyRef) + for _, k := range g.keyRefs { + args = append(args, "-u", k) } if g.passphrase != "" || g.passphraseFile != "" { diff --git a/utils/config.go b/utils/config.go index 4cfac039bd..cb72105f53 100644 --- a/utils/config.go +++ b/utils/config.go @@ -49,9 +49,10 @@ type ConfigStructure struct { // nolint: maligned DownloadSourcePackages bool `json:"downloadSourcePackages" yaml:"download_sourcepackages"` // Signing - GpgProvider string `json:"gpgProvider" yaml:"gpg_provider"` - GpgDisableSign bool `json:"gpgDisableSign" yaml:"gpg_disable_sign"` - GpgDisableVerify bool `json:"gpgDisableVerify" yaml:"gpg_disable_verify"` + GpgProvider string `json:"gpgProvider" yaml:"gpg_provider"` + GpgDisableSign bool `json:"gpgDisableSign" yaml:"gpg_disable_sign"` + GpgDisableVerify bool `json:"gpgDisableVerify" yaml:"gpg_disable_verify"` + GpgKeys []string `json:"gpgKeys" yaml:"gpg_keys"` // Publishing SkipContentsPublishing bool `json:"skipContentsPublishing" yaml:"skip_contents_publishing"` @@ -226,6 +227,7 @@ var Config = ConfigStructure{ GpgProvider: "gpg", GpgDisableSign: false, GpgDisableVerify: false, + GpgKeys: []string{}, DownloadSourcePackages: false, PackagePoolStorage: PackagePoolStorage{ Local: &LocalPoolStorage{Path: ""}, diff --git a/utils/config_test.go b/utils/config_test.go index da6f927e8b..19fd0b52ec 100644 --- a/utils/config_test.go +++ b/utils/config_test.go @@ -19,8 +19,8 @@ func (s *ConfigSuite) TestLoadConfig(c *C) { _, _ = f.WriteString(configFile) _ = f.Close() - // start with empty config - s.config = ConfigStructure{} + // start with empty config + s.config = ConfigStructure{} err := LoadConfig(configname, &s.config) c.Assert(err, IsNil) @@ -32,8 +32,8 @@ func (s *ConfigSuite) TestLoadConfig(c *C) { func (s *ConfigSuite) TestSaveConfig(c *C) { configname := filepath.Join(c.MkDir(), "aptly.json2") - // start with empty config - s.config = ConfigStructure{} + // start with empty config + s.config = ConfigStructure{} s.config.RootDir = "/tmp/aptly" s.config.DownloadConcurrency = 5 @@ -71,93 +71,94 @@ func (s *ConfigSuite) TestSaveConfig(c *C) { _, _ = f.Read(buf) c.Check(string(buf), Equals, ""+ - "{\n" + - " \"rootDir\": \"/tmp/aptly\",\n" + - " \"logLevel\": \"info\",\n" + - " \"logFormat\": \"json\",\n" + - " \"databaseOpenAttempts\": 5,\n" + - " \"architectures\": null,\n" + - " \"skipLegacyPool\": false,\n" + - " \"dependencyFollowSuggests\": false,\n" + - " \"dependencyFollowRecommends\": false,\n" + - " \"dependencyFollowAllVariants\": false,\n" + - " \"dependencyFollowSource\": false,\n" + - " \"dependencyVerboseResolve\": false,\n" + - " \"ppaDistributorID\": \"\",\n" + - " \"ppaCodename\": \"\",\n" + - " \"serveInAPIMode\": false,\n" + - " \"enableMetricsEndpoint\": false,\n" + - " \"enableSwaggerEndpoint\": false,\n" + - " \"AsyncAPI\": false,\n" + - " \"databaseBackend\": {\n" + - " \"type\": \"\",\n" + - " \"dbPath\": \"\",\n" + - " \"url\": \"\"\n" + - " },\n" + - " \"downloader\": \"\",\n" + - " \"downloadConcurrency\": 5,\n" + - " \"downloadSpeedLimit\": 0,\n" + - " \"downloadRetries\": 0,\n" + - " \"downloadSourcePackages\": false,\n" + - " \"gpgProvider\": \"gpg\",\n" + - " \"gpgDisableSign\": false,\n" + - " \"gpgDisableVerify\": false,\n" + - " \"skipContentsPublishing\": false,\n" + - " \"skipBz2Publishing\": false,\n" + - " \"FileSystemPublishEndpoints\": {\n" + - " \"test\": {\n" + - " \"rootDir\": \"/opt/aptly-publish\",\n" + - " \"linkMethod\": \"\",\n" + - " \"verifyMethod\": \"\"\n" + - " }\n" + - " },\n" + - " \"S3PublishEndpoints\": {\n" + - " \"test\": {\n" + - " \"region\": \"us-east-1\",\n" + - " \"bucket\": \"repo\",\n" + - " \"prefix\": \"\",\n" + - " \"acl\": \"\",\n" + - " \"awsAccessKeyID\": \"\",\n" + - " \"awsSecretAccessKey\": \"\",\n" + - " \"awsSessionToken\": \"\",\n" + - " \"endpoint\": \"\",\n" + - " \"storageClass\": \"\",\n" + - " \"encryptionMethod\": \"\",\n" + - " \"plusWorkaround\": false,\n" + - " \"disableMultiDel\": false,\n" + - " \"forceSigV2\": false,\n" + - " \"forceVirtualHostedStyle\": false,\n" + - " \"debug\": false\n" + - " }\n" + - " },\n" + - " \"SwiftPublishEndpoints\": {\n" + - " \"test\": {\n" + - " \"container\": \"repo\",\n" + - " \"prefix\": \"\",\n" + - " \"osname\": \"\",\n" + - " \"password\": \"\",\n" + - " \"tenant\": \"\",\n" + - " \"tenantid\": \"\",\n" + - " \"domain\": \"\",\n" + - " \"domainid\": \"\",\n" + - " \"tenantdomain\": \"\",\n" + - " \"tenantdomainid\": \"\",\n" + - " \"authurl\": \"\"\n" + - " }\n" + - " },\n" + - " \"AzurePublishEndpoints\": {\n" + - " \"test\": {\n" + - " \"container\": \"repo\",\n" + - " \"prefix\": \"\",\n" + - " \"accountName\": \"\",\n" + - " \"accountKey\": \"\",\n" + - " \"endpoint\": \"\"\n" + - " }\n" + - " },\n" + - " \"packagePoolStorage\": {\n" + - " \"type\": \"local\",\n" + - " \"path\": \"/tmp/aptly-pool\"\n" + - " }\n" + + "{\n"+ + " \"rootDir\": \"/tmp/aptly\",\n"+ + " \"logLevel\": \"info\",\n"+ + " \"logFormat\": \"json\",\n"+ + " \"databaseOpenAttempts\": 5,\n"+ + " \"architectures\": null,\n"+ + " \"skipLegacyPool\": false,\n"+ + " \"dependencyFollowSuggests\": false,\n"+ + " \"dependencyFollowRecommends\": false,\n"+ + " \"dependencyFollowAllVariants\": false,\n"+ + " \"dependencyFollowSource\": false,\n"+ + " \"dependencyVerboseResolve\": false,\n"+ + " \"ppaDistributorID\": \"\",\n"+ + " \"ppaCodename\": \"\",\n"+ + " \"serveInAPIMode\": false,\n"+ + " \"enableMetricsEndpoint\": false,\n"+ + " \"enableSwaggerEndpoint\": false,\n"+ + " \"AsyncAPI\": false,\n"+ + " \"databaseBackend\": {\n"+ + " \"type\": \"\",\n"+ + " \"dbPath\": \"\",\n"+ + " \"url\": \"\"\n"+ + " },\n"+ + " \"downloader\": \"\",\n"+ + " \"downloadConcurrency\": 5,\n"+ + " \"downloadSpeedLimit\": 0,\n"+ + " \"downloadRetries\": 0,\n"+ + " \"downloadSourcePackages\": false,\n"+ + " \"gpgProvider\": \"gpg\",\n"+ + " \"gpgDisableSign\": false,\n"+ + " \"gpgDisableVerify\": false,\n"+ + " \"gpgKeys\": null,\n"+ + " \"skipContentsPublishing\": false,\n"+ + " \"skipBz2Publishing\": false,\n"+ + " \"FileSystemPublishEndpoints\": {\n"+ + " \"test\": {\n"+ + " \"rootDir\": \"/opt/aptly-publish\",\n"+ + " \"linkMethod\": \"\",\n"+ + " \"verifyMethod\": \"\"\n"+ + " }\n"+ + " },\n"+ + " \"S3PublishEndpoints\": {\n"+ + " \"test\": {\n"+ + " \"region\": \"us-east-1\",\n"+ + " \"bucket\": \"repo\",\n"+ + " \"prefix\": \"\",\n"+ + " \"acl\": \"\",\n"+ + " \"awsAccessKeyID\": \"\",\n"+ + " \"awsSecretAccessKey\": \"\",\n"+ + " \"awsSessionToken\": \"\",\n"+ + " \"endpoint\": \"\",\n"+ + " \"storageClass\": \"\",\n"+ + " \"encryptionMethod\": \"\",\n"+ + " \"plusWorkaround\": false,\n"+ + " \"disableMultiDel\": false,\n"+ + " \"forceSigV2\": false,\n"+ + " \"forceVirtualHostedStyle\": false,\n"+ + " \"debug\": false\n"+ + " }\n"+ + " },\n"+ + " \"SwiftPublishEndpoints\": {\n"+ + " \"test\": {\n"+ + " \"container\": \"repo\",\n"+ + " \"prefix\": \"\",\n"+ + " \"osname\": \"\",\n"+ + " \"password\": \"\",\n"+ + " \"tenant\": \"\",\n"+ + " \"tenantid\": \"\",\n"+ + " \"domain\": \"\",\n"+ + " \"domainid\": \"\",\n"+ + " \"tenantdomain\": \"\",\n"+ + " \"tenantdomainid\": \"\",\n"+ + " \"authurl\": \"\"\n"+ + " }\n"+ + " },\n"+ + " \"AzurePublishEndpoints\": {\n"+ + " \"test\": {\n"+ + " \"container\": \"repo\",\n"+ + " \"prefix\": \"\",\n"+ + " \"accountName\": \"\",\n"+ + " \"accountKey\": \"\",\n"+ + " \"endpoint\": \"\"\n"+ + " }\n"+ + " },\n"+ + " \"packagePoolStorage\": {\n"+ + " \"type\": \"local\",\n"+ + " \"path\": \"/tmp/aptly-pool\"\n"+ + " }\n"+ "}") } @@ -167,8 +168,8 @@ func (s *ConfigSuite) TestLoadYAMLConfig(c *C) { _, _ = f.WriteString(configFileYAML) _ = f.Close() - // start with empty config - s.config = ConfigStructure{} + // start with empty config + s.config = ConfigStructure{} err := LoadConfig(configname, &s.config) c.Assert(err, IsNil) @@ -183,8 +184,8 @@ func (s *ConfigSuite) TestLoadYAMLErrorConfig(c *C) { _, _ = f.WriteString(configFileYAMLError) _ = f.Close() - // start with empty config - s.config = ConfigStructure{} + // start with empty config + s.config = ConfigStructure{} err := LoadConfig(configname, &s.config) c.Assert(err.Error(), Equals, "invalid yaml (unknown pool storage type: invalid) or json (invalid character 'p' looking for beginning of value)") @@ -196,13 +197,13 @@ func (s *ConfigSuite) TestSaveYAMLConfig(c *C) { _, _ = f.WriteString(configFileYAML) _ = f.Close() - // start with empty config - s.config = ConfigStructure{} + // start with empty config + s.config = ConfigStructure{} err := LoadConfig(configname, &s.config) c.Assert(err, IsNil) - err = SaveConfigYAML(configname, &s.config) + err = SaveConfigYAML(configname, &s.config) c.Assert(err, IsNil) f, _ = os.Open(configname) @@ -218,17 +219,17 @@ func (s *ConfigSuite) TestSaveYAMLConfig(c *C) { } func (s *ConfigSuite) TestSaveYAML2Config(c *C) { - // start with empty config - s.config = ConfigStructure{} + // start with empty config + s.config = ConfigStructure{} s.config.PackagePoolStorage.Local = &LocalPoolStorage{"/tmp/aptly-pool"} - s.config.PackagePoolStorage.Azure = nil + s.config.PackagePoolStorage.Azure = nil configname := filepath.Join(c.MkDir(), "aptly.yaml4") - err := SaveConfigYAML(configname, &s.config) + err := SaveConfigYAML(configname, &s.config) c.Assert(err, IsNil) - f, _ := os.Open(configname) + f, _ := os.Open(configname) defer func() { _ = f.Close() }() @@ -237,44 +238,45 @@ func (s *ConfigSuite) TestSaveYAML2Config(c *C) { buf := make([]byte, st.Size()) _, _ = f.Read(buf) - c.Check(string(buf), Equals, "" + - "root_dir: \"\"\n" + - "log_level: \"\"\n" + - "log_format: \"\"\n" + - "database_open_attempts: 0\n" + - "architectures: []\n" + - "skip_legacy_pool: false\n" + - "dep_follow_suggests: false\n" + - "dep_follow_recommends: false\n" + - "dep_follow_all_variants: false\n" + - "dep_follow_source: false\n" + - "dep_verboseresolve: false\n" + - "ppa_distributor_id: \"\"\n" + - "ppa_codename: \"\"\n" + - "serve_in_api_mode: false\n" + - "enable_metrics_endpoint: false\n" + - "enable_swagger_endpoint: false\n" + - "async_api: false\n" + - "database_backend:\n" + - " type: \"\"\n" + - " db_path: \"\"\n" + - " url: \"\"\n" + - "downloader: \"\"\n" + - "download_concurrency: 0\n" + - "download_limit: 0\n" + - "download_retries: 0\n" + - "download_sourcepackages: false\n" + - "gpg_provider: \"\"\n" + - "gpg_disable_sign: false\n" + - "gpg_disable_verify: false\n" + - "skip_contents_publishing: false\n" + - "skip_bz2_publishing: false\n" + - "filesystem_publish_endpoints: {}\n" + - "s3_publish_endpoints: {}\n" + - "swift_publish_endpoints: {}\n" + - "azure_publish_endpoints: {}\n" + - "packagepool_storage:\n" + - " type: local\n" + + c.Check(string(buf), Equals, ""+ + "root_dir: \"\"\n"+ + "log_level: \"\"\n"+ + "log_format: \"\"\n"+ + "database_open_attempts: 0\n"+ + "architectures: []\n"+ + "skip_legacy_pool: false\n"+ + "dep_follow_suggests: false\n"+ + "dep_follow_recommends: false\n"+ + "dep_follow_all_variants: false\n"+ + "dep_follow_source: false\n"+ + "dep_verboseresolve: false\n"+ + "ppa_distributor_id: \"\"\n"+ + "ppa_codename: \"\"\n"+ + "serve_in_api_mode: false\n"+ + "enable_metrics_endpoint: false\n"+ + "enable_swagger_endpoint: false\n"+ + "async_api: false\n"+ + "database_backend:\n"+ + " type: \"\"\n"+ + " db_path: \"\"\n"+ + " url: \"\"\n"+ + "downloader: \"\"\n"+ + "download_concurrency: 0\n"+ + "download_limit: 0\n"+ + "download_retries: 0\n"+ + "download_sourcepackages: false\n"+ + "gpg_provider: \"\"\n"+ + "gpg_disable_sign: false\n"+ + "gpg_disable_verify: false\n"+ + "gpg_keys: []\n"+ + "skip_contents_publishing: false\n"+ + "skip_bz2_publishing: false\n"+ + "filesystem_publish_endpoints: {}\n"+ + "s3_publish_endpoints: {}\n"+ + "swift_publish_endpoints: {}\n"+ + "azure_publish_endpoints: {}\n"+ + "packagepool_storage:\n"+ + " type: local\n"+ " path: /tmp/aptly-pool\n") } @@ -283,8 +285,8 @@ func (s *ConfigSuite) TestLoadEmptyConfig(c *C) { f, _ := os.Create(configname) _ = f.Close() - // start with empty config - s.config = ConfigStructure{} + // start with empty config + s.config = ConfigStructure{} err := LoadConfig(configname, &s.config) c.Assert(err.Error(), Equals, "invalid yaml (EOF) or json (EOF)") @@ -322,6 +324,7 @@ download_sourcepackages: true gpg_provider: gpg gpg_disable_sign: true gpg_disable_verify: true +gpg_keys: [] skip_contents_publishing: true skip_bz2_publishing: true filesystem_publish_endpoints: From 6ab9830cb881049e729e2276682c53778afbeba3 Mon Sep 17 00:00:00 2001 From: Ales Bregar Date: Mon, 11 Aug 2025 11:06:56 +0200 Subject: [PATCH 02/12] white space revert to minimize change --- AUTHORS | 3 +- utils/config_test.go | 292 +++++++++++++++++++++---------------------- 2 files changed, 148 insertions(+), 147 deletions(-) diff --git a/AUTHORS b/AUTHORS index 8eef529da6..7cd86f2e11 100644 --- a/AUTHORS +++ b/AUTHORS @@ -69,4 +69,5 @@ List of contributors, in chronological order: * Leigh London (https://github.com/leighlondon) * Gordian Schoenherr (https://github.com/schoenherrg) * Silke Hofstra (https://github.com/silkeh) -* Itay Porezky (https://github.com/itayporezky) \ No newline at end of file +* Itay Porezky (https://github.com/itayporezky) +* Ales Bregar (https://github.com/abregar) \ No newline at end of file diff --git a/utils/config_test.go b/utils/config_test.go index 19fd0b52ec..1bb2ffed3e 100644 --- a/utils/config_test.go +++ b/utils/config_test.go @@ -19,8 +19,8 @@ func (s *ConfigSuite) TestLoadConfig(c *C) { _, _ = f.WriteString(configFile) _ = f.Close() - // start with empty config - s.config = ConfigStructure{} + // start with empty config + s.config = ConfigStructure{} err := LoadConfig(configname, &s.config) c.Assert(err, IsNil) @@ -32,8 +32,8 @@ func (s *ConfigSuite) TestLoadConfig(c *C) { func (s *ConfigSuite) TestSaveConfig(c *C) { configname := filepath.Join(c.MkDir(), "aptly.json2") - // start with empty config - s.config = ConfigStructure{} + // start with empty config + s.config = ConfigStructure{} s.config.RootDir = "/tmp/aptly" s.config.DownloadConcurrency = 5 @@ -71,94 +71,94 @@ func (s *ConfigSuite) TestSaveConfig(c *C) { _, _ = f.Read(buf) c.Check(string(buf), Equals, ""+ - "{\n"+ - " \"rootDir\": \"/tmp/aptly\",\n"+ - " \"logLevel\": \"info\",\n"+ - " \"logFormat\": \"json\",\n"+ - " \"databaseOpenAttempts\": 5,\n"+ - " \"architectures\": null,\n"+ - " \"skipLegacyPool\": false,\n"+ - " \"dependencyFollowSuggests\": false,\n"+ - " \"dependencyFollowRecommends\": false,\n"+ - " \"dependencyFollowAllVariants\": false,\n"+ - " \"dependencyFollowSource\": false,\n"+ - " \"dependencyVerboseResolve\": false,\n"+ - " \"ppaDistributorID\": \"\",\n"+ - " \"ppaCodename\": \"\",\n"+ - " \"serveInAPIMode\": false,\n"+ - " \"enableMetricsEndpoint\": false,\n"+ - " \"enableSwaggerEndpoint\": false,\n"+ - " \"AsyncAPI\": false,\n"+ - " \"databaseBackend\": {\n"+ - " \"type\": \"\",\n"+ - " \"dbPath\": \"\",\n"+ - " \"url\": \"\"\n"+ - " },\n"+ - " \"downloader\": \"\",\n"+ - " \"downloadConcurrency\": 5,\n"+ - " \"downloadSpeedLimit\": 0,\n"+ - " \"downloadRetries\": 0,\n"+ - " \"downloadSourcePackages\": false,\n"+ - " \"gpgProvider\": \"gpg\",\n"+ - " \"gpgDisableSign\": false,\n"+ - " \"gpgDisableVerify\": false,\n"+ - " \"gpgKeys\": null,\n"+ - " \"skipContentsPublishing\": false,\n"+ - " \"skipBz2Publishing\": false,\n"+ - " \"FileSystemPublishEndpoints\": {\n"+ - " \"test\": {\n"+ - " \"rootDir\": \"/opt/aptly-publish\",\n"+ - " \"linkMethod\": \"\",\n"+ - " \"verifyMethod\": \"\"\n"+ - " }\n"+ - " },\n"+ - " \"S3PublishEndpoints\": {\n"+ - " \"test\": {\n"+ - " \"region\": \"us-east-1\",\n"+ - " \"bucket\": \"repo\",\n"+ - " \"prefix\": \"\",\n"+ - " \"acl\": \"\",\n"+ - " \"awsAccessKeyID\": \"\",\n"+ - " \"awsSecretAccessKey\": \"\",\n"+ - " \"awsSessionToken\": \"\",\n"+ - " \"endpoint\": \"\",\n"+ - " \"storageClass\": \"\",\n"+ - " \"encryptionMethod\": \"\",\n"+ - " \"plusWorkaround\": false,\n"+ - " \"disableMultiDel\": false,\n"+ - " \"forceSigV2\": false,\n"+ - " \"forceVirtualHostedStyle\": false,\n"+ - " \"debug\": false\n"+ - " }\n"+ - " },\n"+ - " \"SwiftPublishEndpoints\": {\n"+ - " \"test\": {\n"+ - " \"container\": \"repo\",\n"+ - " \"prefix\": \"\",\n"+ - " \"osname\": \"\",\n"+ - " \"password\": \"\",\n"+ - " \"tenant\": \"\",\n"+ - " \"tenantid\": \"\",\n"+ - " \"domain\": \"\",\n"+ - " \"domainid\": \"\",\n"+ - " \"tenantdomain\": \"\",\n"+ - " \"tenantdomainid\": \"\",\n"+ - " \"authurl\": \"\"\n"+ - " }\n"+ - " },\n"+ - " \"AzurePublishEndpoints\": {\n"+ - " \"test\": {\n"+ - " \"container\": \"repo\",\n"+ - " \"prefix\": \"\",\n"+ - " \"accountName\": \"\",\n"+ - " \"accountKey\": \"\",\n"+ - " \"endpoint\": \"\"\n"+ - " }\n"+ - " },\n"+ - " \"packagePoolStorage\": {\n"+ - " \"type\": \"local\",\n"+ - " \"path\": \"/tmp/aptly-pool\"\n"+ - " }\n"+ + "{\n" + + " \"rootDir\": \"/tmp/aptly\",\n" + + " \"logLevel\": \"info\",\n" + + " \"logFormat\": \"json\",\n" + + " \"databaseOpenAttempts\": 5,\n" + + " \"architectures\": null,\n" + + " \"skipLegacyPool\": false,\n" + + " \"dependencyFollowSuggests\": false,\n" + + " \"dependencyFollowRecommends\": false,\n" + + " \"dependencyFollowAllVariants\": false,\n" + + " \"dependencyFollowSource\": false,\n" + + " \"dependencyVerboseResolve\": false,\n" + + " \"ppaDistributorID\": \"\",\n" + + " \"ppaCodename\": \"\",\n" + + " \"serveInAPIMode\": false,\n" + + " \"enableMetricsEndpoint\": false,\n" + + " \"enableSwaggerEndpoint\": false,\n" + + " \"AsyncAPI\": false,\n" + + " \"databaseBackend\": {\n" + + " \"type\": \"\",\n" + + " \"dbPath\": \"\",\n" + + " \"url\": \"\"\n" + + " },\n" + + " \"downloader\": \"\",\n" + + " \"downloadConcurrency\": 5,\n" + + " \"downloadSpeedLimit\": 0,\n" + + " \"downloadRetries\": 0,\n" + + " \"downloadSourcePackages\": false,\n" + + " \"gpgProvider\": \"gpg\",\n" + + " \"gpgDisableSign\": false,\n" + + " \"gpgDisableVerify\": false,\n" + + " \"gpgKeys\": null,\n" + + " \"skipContentsPublishing\": false,\n" + + " \"skipBz2Publishing\": false,\n" + + " \"FileSystemPublishEndpoints\": {\n" + + " \"test\": {\n" + + " \"rootDir\": \"/opt/aptly-publish\",\n" + + " \"linkMethod\": \"\",\n" + + " \"verifyMethod\": \"\"\n" + + " }\n" + + " },\n" + + " \"S3PublishEndpoints\": {\n" + + " \"test\": {\n" + + " \"region\": \"us-east-1\",\n" + + " \"bucket\": \"repo\",\n" + + " \"prefix\": \"\",\n" + + " \"acl\": \"\",\n" + + " \"awsAccessKeyID\": \"\",\n" + + " \"awsSecretAccessKey\": \"\",\n" + + " \"awsSessionToken\": \"\",\n" + + " \"endpoint\": \"\",\n" + + " \"storageClass\": \"\",\n" + + " \"encryptionMethod\": \"\",\n" + + " \"plusWorkaround\": false,\n" + + " \"disableMultiDel\": false,\n" + + " \"forceSigV2\": false,\n" + + " \"forceVirtualHostedStyle\": false,\n" + + " \"debug\": false\n" + + " }\n" + + " },\n" + + " \"SwiftPublishEndpoints\": {\n" + + " \"test\": {\n" + + " \"container\": \"repo\",\n" + + " \"prefix\": \"\",\n" + + " \"osname\": \"\",\n" + + " \"password\": \"\",\n" + + " \"tenant\": \"\",\n" + + " \"tenantid\": \"\",\n" + + " \"domain\": \"\",\n" + + " \"domainid\": \"\",\n" + + " \"tenantdomain\": \"\",\n" + + " \"tenantdomainid\": \"\",\n" + + " \"authurl\": \"\"\n" + + " }\n" + + " },\n" + + " \"AzurePublishEndpoints\": {\n" + + " \"test\": {\n" + + " \"container\": \"repo\",\n" + + " \"prefix\": \"\",\n" + + " \"accountName\": \"\",\n" + + " \"accountKey\": \"\",\n" + + " \"endpoint\": \"\"\n" + + " }\n" + + " },\n" + + " \"packagePoolStorage\": {\n" + + " \"type\": \"local\",\n" + + " \"path\": \"/tmp/aptly-pool\"\n" + + " }\n" + "}") } @@ -168,8 +168,8 @@ func (s *ConfigSuite) TestLoadYAMLConfig(c *C) { _, _ = f.WriteString(configFileYAML) _ = f.Close() - // start with empty config - s.config = ConfigStructure{} + // start with empty config + s.config = ConfigStructure{} err := LoadConfig(configname, &s.config) c.Assert(err, IsNil) @@ -184,8 +184,8 @@ func (s *ConfigSuite) TestLoadYAMLErrorConfig(c *C) { _, _ = f.WriteString(configFileYAMLError) _ = f.Close() - // start with empty config - s.config = ConfigStructure{} + // start with empty config + s.config = ConfigStructure{} err := LoadConfig(configname, &s.config) c.Assert(err.Error(), Equals, "invalid yaml (unknown pool storage type: invalid) or json (invalid character 'p' looking for beginning of value)") @@ -197,13 +197,13 @@ func (s *ConfigSuite) TestSaveYAMLConfig(c *C) { _, _ = f.WriteString(configFileYAML) _ = f.Close() - // start with empty config - s.config = ConfigStructure{} + // start with empty config + s.config = ConfigStructure{} err := LoadConfig(configname, &s.config) c.Assert(err, IsNil) - err = SaveConfigYAML(configname, &s.config) + err = SaveConfigYAML(configname, &s.config) c.Assert(err, IsNil) f, _ = os.Open(configname) @@ -219,17 +219,17 @@ func (s *ConfigSuite) TestSaveYAMLConfig(c *C) { } func (s *ConfigSuite) TestSaveYAML2Config(c *C) { - // start with empty config - s.config = ConfigStructure{} + // start with empty config + s.config = ConfigStructure{} s.config.PackagePoolStorage.Local = &LocalPoolStorage{"/tmp/aptly-pool"} - s.config.PackagePoolStorage.Azure = nil + s.config.PackagePoolStorage.Azure = nil configname := filepath.Join(c.MkDir(), "aptly.yaml4") - err := SaveConfigYAML(configname, &s.config) + err := SaveConfigYAML(configname, &s.config) c.Assert(err, IsNil) - f, _ := os.Open(configname) + f, _ := os.Open(configname) defer func() { _ = f.Close() }() @@ -238,45 +238,45 @@ func (s *ConfigSuite) TestSaveYAML2Config(c *C) { buf := make([]byte, st.Size()) _, _ = f.Read(buf) - c.Check(string(buf), Equals, ""+ - "root_dir: \"\"\n"+ - "log_level: \"\"\n"+ - "log_format: \"\"\n"+ - "database_open_attempts: 0\n"+ - "architectures: []\n"+ - "skip_legacy_pool: false\n"+ - "dep_follow_suggests: false\n"+ - "dep_follow_recommends: false\n"+ - "dep_follow_all_variants: false\n"+ - "dep_follow_source: false\n"+ - "dep_verboseresolve: false\n"+ - "ppa_distributor_id: \"\"\n"+ - "ppa_codename: \"\"\n"+ - "serve_in_api_mode: false\n"+ - "enable_metrics_endpoint: false\n"+ - "enable_swagger_endpoint: false\n"+ - "async_api: false\n"+ - "database_backend:\n"+ - " type: \"\"\n"+ - " db_path: \"\"\n"+ - " url: \"\"\n"+ - "downloader: \"\"\n"+ - "download_concurrency: 0\n"+ - "download_limit: 0\n"+ - "download_retries: 0\n"+ - "download_sourcepackages: false\n"+ - "gpg_provider: \"\"\n"+ - "gpg_disable_sign: false\n"+ - "gpg_disable_verify: false\n"+ - "gpg_keys: []\n"+ - "skip_contents_publishing: false\n"+ - "skip_bz2_publishing: false\n"+ - "filesystem_publish_endpoints: {}\n"+ - "s3_publish_endpoints: {}\n"+ - "swift_publish_endpoints: {}\n"+ - "azure_publish_endpoints: {}\n"+ - "packagepool_storage:\n"+ - " type: local\n"+ + c.Check(string(buf), Equals, "" + + "root_dir: \"\"\n" + + "log_level: \"\"\n" + + "log_format: \"\"\n" + + "database_open_attempts: 0\n" + + "architectures: []\n" + + "skip_legacy_pool: false\n" + + "dep_follow_suggests: false\n" + + "dep_follow_recommends: false\n" + + "dep_follow_all_variants: false\n" + + "dep_follow_source: false\n" + + "dep_verboseresolve: false\n" + + "ppa_distributor_id: \"\"\n" + + "ppa_codename: \"\"\n" + + "serve_in_api_mode: false\n" + + "enable_metrics_endpoint: false\n" + + "enable_swagger_endpoint: false\n" + + "async_api: false\n" + + "database_backend:\n" + + " type: \"\"\n" + + " db_path: \"\"\n" + + " url: \"\"\n" + + "downloader: \"\"\n" + + "download_concurrency: 0\n" + + "download_limit: 0\n" + + "download_retries: 0\n" + + "download_sourcepackages: false\n" + + "gpg_provider: \"\"\n" + + "gpg_disable_sign: false\n" + + "gpg_disable_verify: false\n" + + "gpg_keys: []\n" + + "skip_contents_publishing: false\n" + + "skip_bz2_publishing: false\n" + + "filesystem_publish_endpoints: {}\n" + + "s3_publish_endpoints: {}\n" + + "swift_publish_endpoints: {}\n" + + "azure_publish_endpoints: {}\n" + + "packagepool_storage:\n" + + " type: local\n" + " path: /tmp/aptly-pool\n") } @@ -285,8 +285,8 @@ func (s *ConfigSuite) TestLoadEmptyConfig(c *C) { f, _ := os.Create(configname) _ = f.Close() - // start with empty config - s.config = ConfigStructure{} + // start with empty config + s.config = ConfigStructure{} err := LoadConfig(configname, &s.config) c.Assert(err.Error(), Equals, "invalid yaml (EOF) or json (EOF)") @@ -379,4 +379,4 @@ packagepool_storage: ` const configFileYAMLError = `packagepool_storage: type: invalid -` +` \ No newline at end of file From db02c8ad08953ab604617aca08aad626ada2db38 Mon Sep 17 00:00:00 2001 From: Ales Bregar Date: Mon, 11 Aug 2025 11:30:35 +0200 Subject: [PATCH 03/12] documentation updated --- docs/Publish.md | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/docs/Publish.md b/docs/Publish.md index 0077f4d9b6..4175b63124 100644 --- a/docs/Publish.md +++ b/docs/Publish.md @@ -11,7 +11,22 @@ Repositories can be published to local directories, Amazon S3 buckets, Azure or GPG key is required to sign any published repository. The key pari should be generated before publishing. -Publiс part of the key should be exported from your keyring using `gpg --export --armor` and imported on the system which uses a published repository. +Public part of the key should be exported from your keyring using `gpg --export --armor` and imported on the system which uses a published repository. + +* Multiple signing keys can be defined in aptly.conf using the gpgKeys array: +``` +"gpgKeys": [ + "KEY_ID_x", + "KEY_ID_y" +] +``` + +* It is also possible to pass multiple keys via the CLI using the repeatable `--gpg-key` flag: +``` +aptly publish repo my-repo --gpg-key=KEY_ID_a --gpg-key=KEY_ID_b +``` +* If `--gpg-key` is specified on the command line, it takes precedence over any gpgKeys configuration in `aptly.conf`. +* With multi-key support, aptly will sign all Release files (both clearsigned and detached signatures) with each provided key, ensuring a smooth key rotation process while maintaining compatibility for existing clients. #### Parameters From f17fd977e380900f91a0e3fdbd5dec4dc94ab79a Mon Sep 17 00:00:00 2001 From: Ales Bregar Date: Mon, 11 Aug 2025 12:00:33 +0200 Subject: [PATCH 04/12] system test configuration fix --- system/t02_config/ConfigShowTest_gold | 1 + system/t02_config/ConfigShowYAMLTest_gold | 1 + 2 files changed, 2 insertions(+) diff --git a/system/t02_config/ConfigShowTest_gold b/system/t02_config/ConfigShowTest_gold index 5a4a2273ae..ba3b88d65b 100644 --- a/system/t02_config/ConfigShowTest_gold +++ b/system/t02_config/ConfigShowTest_gold @@ -29,6 +29,7 @@ "gpgProvider": "gpg", "gpgDisableSign": false, "gpgDisableVerify": false, + "gpgKeys": [], "skipContentsPublishing": false, "skipBz2Publishing": false, "FileSystemPublishEndpoints": {}, diff --git a/system/t02_config/ConfigShowYAMLTest_gold b/system/t02_config/ConfigShowYAMLTest_gold index 615982b0db..02efe23d30 100644 --- a/system/t02_config/ConfigShowYAMLTest_gold +++ b/system/t02_config/ConfigShowYAMLTest_gold @@ -27,6 +27,7 @@ download_sourcepackages: false gpg_provider: gpg gpg_disable_sign: false gpg_disable_verify: false +gpg_keys: [] skip_contents_publishing: false skip_bz2_publishing: false filesystem_publish_endpoints: {} From c44d40e03ecb84e84bb0e7c0bfe1bcef17d7f235 Mon Sep 17 00:00:00 2001 From: Ales Bregar Date: Mon, 11 Aug 2025 12:20:52 +0200 Subject: [PATCH 05/12] system test unexpected string fix (would be helpful, but not changing the test just for this) --- cmd/publish.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/cmd/publish.go b/cmd/publish.go index 70b65e34a4..6f4904ada4 100644 --- a/cmd/publish.go +++ b/cmd/publish.go @@ -1,7 +1,6 @@ package cmd import ( - "fmt" "strings" "github.com/aptly-dev/aptly/pgp" @@ -26,9 +25,6 @@ func getSigner(flags *flag.FlagSet) (pgp.Signer, error) { gpgKeys = context.Config().GpgKeys } - if len(gpgKeys) > 0 { - fmt.Printf("Signing with following gpg keys %s\n", strings.Join(gpgKeys, ", ")) - } for _, gpgKey := range gpgKeys { signer.SetKey(gpgKey) } From 1515b5263c2f6cb3b178688f7d0022eced24d1b7 Mon Sep 17 00:00:00 2001 From: Ales Bregar Date: Mon, 11 Aug 2025 15:08:11 +0200 Subject: [PATCH 06/12] system test t12_api sends empty keyRef string, making gpg fail --- pgp/gnupg.go | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/pgp/gnupg.go b/pgp/gnupg.go index dbff8ddcc1..bab2db21b2 100644 --- a/pgp/gnupg.go +++ b/pgp/gnupg.go @@ -35,11 +35,14 @@ func (g *GpgSigner) SetBatch(batch bool) { // SetKey sets key ID to use when signing files func (g *GpgSigner) SetKey(keyRef string) { - if g.keyRefs == nil { - g.keyRefs = []string{strings.TrimSpace(keyRef)} - } else { - g.keyRefs = append(g.keyRefs, strings.TrimSpace(keyRef)) - } + keyRef = strings.TrimSpace(keyRef) + if keyRef != "" { + if g.keyRefs == nil { + g.keyRefs = []string{keyRef} + } else { + g.keyRefs = append(g.keyRefs, keyRef) + } + } } // SetKeyRing allows to set custom keyring and secretkeyring From 8fbb0d2ba0d0fc19712879a230427ae0513447f8 Mon Sep 17 00:00:00 2001 From: Ales Bregar Date: Tue, 12 Aug 2025 14:14:52 +0200 Subject: [PATCH 07/12] review fix --- cmd/publish_repo.go | 2 +- cmd/publish_snapshot.go | 2 +- cmd/publish_switch.go | 2 +- cmd/publish_update.go | 2 +- utils/config_test.go | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/cmd/publish_repo.go b/cmd/publish_repo.go index 7e7111ba3a..9e8457f25f 100644 --- a/cmd/publish_repo.go +++ b/cmd/publish_repo.go @@ -34,7 +34,7 @@ Example: } cmd.Flag.String("distribution", "", "distribution name to publish") cmd.Flag.String("component", "", "component name to publish (for multi-component publishing, separate components with commas)") - cmd.Flag.Var(&gpgKeyFlag{}, "gpg-key", "GPG key ID to use when signing the release (repeatable, can be specified multiple times)") + cmd.Flag.Var(&gpgKeyFlag{}, "gpg-key", "GPG key ID to use when signing the release (flag is repeatable, can be specified multiple times)") cmd.Flag.Var(&keyRingsFlag{}, "keyring", "GPG keyring to use (instead of default)") cmd.Flag.String("secret-keyring", "", "GPG secret keyring to use (instead of default)") cmd.Flag.String("passphrase", "", "GPG passphrase for the key (warning: could be insecure)") diff --git a/cmd/publish_snapshot.go b/cmd/publish_snapshot.go index 6ca9d7a28d..0f251d28fd 100644 --- a/cmd/publish_snapshot.go +++ b/cmd/publish_snapshot.go @@ -230,7 +230,7 @@ Example: } cmd.Flag.String("distribution", "", "distribution name to publish") cmd.Flag.String("component", "", "component name to publish (for multi-component publishing, separate components with commas)") - cmd.Flag.Var(&gpgKeyFlag{}, "gpg-key", "GPG key ID to use when signing the release (repeatable, can be specified multiple times)") + cmd.Flag.Var(&gpgKeyFlag{}, "gpg-key", "GPG key ID to use when signing the release (flag is repeatable, can be specified multiple times)") cmd.Flag.Var(&keyRingsFlag{}, "keyring", "GPG keyring to use (instead of default)") cmd.Flag.String("secret-keyring", "", "GPG secret keyring to use (instead of default)") cmd.Flag.String("passphrase", "", "GPG passphrase for the key (warning: could be insecure)") diff --git a/cmd/publish_switch.go b/cmd/publish_switch.go index fe80036967..fbd8719cec 100644 --- a/cmd/publish_switch.go +++ b/cmd/publish_switch.go @@ -151,7 +151,7 @@ This command would switch published repository (with one component) named ppa/wh `, Flag: *flag.NewFlagSet("aptly-publish-switch", flag.ExitOnError), } - cmd.Flag.Var(&gpgKeyFlag{}, "gpg-key", "GPG key ID to use when signing the release (repeatable, can be specified multiple times)") + cmd.Flag.Var(&gpgKeyFlag{}, "gpg-key", "GPG key ID to use when signing the release (flag is repeatable, can be specified multiple times)") cmd.Flag.Var(&keyRingsFlag{}, "keyring", "GPG keyring to use (instead of default)") cmd.Flag.String("secret-keyring", "", "GPG secret keyring to use (instead of default)") cmd.Flag.String("passphrase", "", "GPG passphrase for the key (warning: could be insecure)") diff --git a/cmd/publish_update.go b/cmd/publish_update.go index d15c7ab46d..1148196be4 100644 --- a/cmd/publish_update.go +++ b/cmd/publish_update.go @@ -115,7 +115,7 @@ Example: `, Flag: *flag.NewFlagSet("aptly-publish-update", flag.ExitOnError), } - cmd.Flag.Var(&gpgKeyFlag{}, "gpg-key", "GPG key ID to use when signing the release (repeatable, can be specified multiple times)") + cmd.Flag.Var(&gpgKeyFlag{}, "gpg-key", "GPG key ID to use when signing the release (flag is repeatable, can be specified multiple times)") cmd.Flag.Var(&keyRingsFlag{}, "keyring", "GPG keyring to use (instead of default)") cmd.Flag.String("secret-keyring", "", "GPG secret keyring to use (instead of default)") cmd.Flag.String("passphrase", "", "GPG passphrase for the key (warning: could be insecure)") diff --git a/utils/config_test.go b/utils/config_test.go index 1bb2ffed3e..294f41675c 100644 --- a/utils/config_test.go +++ b/utils/config_test.go @@ -379,4 +379,4 @@ packagepool_storage: ` const configFileYAMLError = `packagepool_storage: type: invalid -` \ No newline at end of file +` From f5a4292c7c56e2a9072968c970e867bb9efc861e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Roth?= Date: Mon, 9 Jun 2025 18:59:32 +0200 Subject: [PATCH 08/12] update Releasing.md --- Releasing.md | 1 + 1 file changed, 1 insertion(+) diff --git a/Releasing.md b/Releasing.md index 5176ff6348..ab7c5230b5 100644 --- a/Releasing.md +++ b/Releasing.md @@ -13,5 +13,6 @@ git push origin v$version master - run swagger locally (`make docker-serve`) - copy generated docs/swagger.json to https://github.com/aptly-dev/www.aptly.info/tree/master/static/swagger/aptly_1.x.y.json - add new version to select tag in content/doc/api/swagger.md line 48 +- update version in content/download.md - push commit to master - create release announcement on https://github.com/aptly-dev/aptly/discussions From 7166330e92c4a379a5d5036e7571d6763fe4dd12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Roth?= Date: Tue, 12 Aug 2025 14:00:52 +0200 Subject: [PATCH 09/12] ci: remove EOL debian/buster --- .github/workflows/ci.yml | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fa9dd64c0e..31aa2ab6de 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -109,10 +109,10 @@ jobs: strategy: fail-fast: false matrix: - name: ["Debian 13/testing", "Debian 12/bookworm", "Debian 11/bullseye", "Debian 10/buster", "Ubuntu 24.04", "Ubuntu 22.04", "Ubuntu 20.04"] + name: ["Debian 13/trixie", "Debian 12/bookworm", "Debian 11/bullseye", "Ubuntu 24.04", "Ubuntu 22.04", "Ubuntu 20.04"] arch: ["amd64", "i386" , "arm64" , "armhf"] include: - - name: "Debian 13/testing" + - name: "Debian 13/trixie" suite: trixie image: debian:trixie-slim - name: "Debian 12/bookworm" @@ -121,9 +121,6 @@ jobs: - name: "Debian 11/bullseye" suite: bullseye image: debian:bullseye-slim - - name: "Debian 10/buster" - suite: buster - image: debian:buster-slim - name: "Ubuntu 24.04" suite: noble image: ubuntu:24.04 @@ -135,6 +132,7 @@ jobs: image: ubuntu:20.04 container: image: ${{ matrix.image }} + options: --user root env: APT_LISTCHANGES_FRONTEND: none DEBIAN_FRONTEND: noninteractive From 47aa23132fe64a2eb01dd936f3446ecd3463f59d Mon Sep 17 00:00:00 2001 From: Ales Bregar Date: Tue, 12 Aug 2025 17:58:45 +0200 Subject: [PATCH 10/12] updating REST api with multiple gpg keys support, due backwards compatibility introducing CSV under same key (gpg-key) --- api/publish.go | 20 +++++++++++++++++--- docs/Publish.md | 6 +++++- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/api/publish.go b/api/publish.go index 1a2b5287bb..81a03354ab 100644 --- a/api/publish.go +++ b/api/publish.go @@ -16,8 +16,8 @@ import ( type signingParams struct { // Don't sign published repository Skip bool ` json:"Skip" example:"false"` - // GPG key ID to use when signing the release, if not specified default key is used - GpgKey string ` json:"GpgKey" example:"A0546A43624A8331"` + // GPG key ID(s) to use when signing the release, CSV if multiple keys, if not specified default configured key(s) are used + GpgKey string ` json:"GpgKey" example:"KEY_ID_a,KEY_ID_b"` // GPG keyring to use (instead of default) Keyring string ` json:"Keyring" example:"trustedkeys.gpg"` // GPG secret keyring to use (instead of default) Note: depreciated with gpg2 @@ -41,7 +41,21 @@ func getSigner(options *signingParams) (pgp.Signer, error) { } signer := context.GetSigner() - signer.SetKey(options.GpgKey) + + var multiGpgKeys []string + // REST params have priority over config + if options.GpgKey != "" { + for _, p := range strings.Split(options.GpgKey, ",") { + if t := strings.TrimSpace(p); t != "" { + multiGpgKeys = append(multiGpgKeys, t) + } + } + } else if len(context.Config().GpgKeys) > 0 { + multiGpgKeys = context.Config().GpgKeys + } + for _, gpgKey := range multiGpgKeys { + signer.SetKey(gpgKey) + } signer.SetKeyRing(options.Keyring, options.SecretKeyring) signer.SetPassphrase(options.Passphrase, options.PassphraseFile) diff --git a/docs/Publish.md b/docs/Publish.md index 4175b63124..40cacbfe8b 100644 --- a/docs/Publish.md +++ b/docs/Publish.md @@ -25,7 +25,11 @@ Public part of the key should be exported from your keyring using `gpg --export ``` aptly publish repo my-repo --gpg-key=KEY_ID_a --gpg-key=KEY_ID_b ``` -* If `--gpg-key` is specified on the command line, it takes precedence over any gpgKeys configuration in `aptly.conf`. +* When using the REST API, the `gpgKey` parameter supports a comma-separated list of key IDs: +``` +"gpgKey": "KEY_ID_a,KEY_ID_b" +``` +* If `--gpg-key` is specified on the command line, or `gpgKey` is provided via the REST API, it takes precedence over any gpgKeys configuration in aptly.conf. * With multi-key support, aptly will sign all Release files (both clearsigned and detached signatures) with each provided key, ensuring a smooth key rotation process while maintaining compatibility for existing clients. #### Parameters From 09313c26ef5eb5a0a691e99f56b1173b403c8a6b Mon Sep 17 00:00:00 2001 From: Ales Bregar Date: Sat, 16 Aug 2025 09:01:06 +0200 Subject: [PATCH 11/12] clearer REST api docs, put whitespace to docs to show that keyId strings are trimmed --- api/publish.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/publish.go b/api/publish.go index 81a03354ab..56de811796 100644 --- a/api/publish.go +++ b/api/publish.go @@ -16,8 +16,8 @@ import ( type signingParams struct { // Don't sign published repository Skip bool ` json:"Skip" example:"false"` - // GPG key ID(s) to use when signing the release, CSV if multiple keys, if not specified default configured key(s) are used - GpgKey string ` json:"GpgKey" example:"KEY_ID_a,KEY_ID_b"` + // GPG key ID(s) to use when signing the release, separated by comma, and if not specified, default configured key(s) are used + GpgKey string ` json:"GpgKey" example:"KEY_ID_a, KEY_ID_b"` // GPG keyring to use (instead of default) Keyring string ` json:"Keyring" example:"trustedkeys.gpg"` // GPG secret keyring to use (instead of default) Note: depreciated with gpg2 From 0ae0cda0c562f269d731e4debf3cb08764d9ca3b Mon Sep 17 00:00:00 2001 From: Ales Bregar Date: Mon, 1 Sep 2025 08:30:11 +0200 Subject: [PATCH 12/12] white space align to prev --- utils/config_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/utils/config_test.go b/utils/config_test.go index 4dade2733d..19fd0b52ec 100644 --- a/utils/config_test.go +++ b/utils/config_test.go @@ -102,7 +102,7 @@ func (s *ConfigSuite) TestSaveConfig(c *C) { " \"gpgProvider\": \"gpg\",\n"+ " \"gpgDisableSign\": false,\n"+ " \"gpgDisableVerify\": false,\n"+ - " \"gpgKeys\": null,\n"+ + " \"gpgKeys\": null,\n"+ " \"skipContentsPublishing\": false,\n"+ " \"skipBz2Publishing\": false,\n"+ " \"FileSystemPublishEndpoints\": {\n"+ @@ -268,7 +268,7 @@ func (s *ConfigSuite) TestSaveYAML2Config(c *C) { "gpg_provider: \"\"\n"+ "gpg_disable_sign: false\n"+ "gpg_disable_verify: false\n"+ - "gpg_keys: []\n"+ + "gpg_keys: []\n"+ "skip_contents_publishing: false\n"+ "skip_bz2_publishing: false\n"+ "filesystem_publish_endpoints: {}\n"+