diff --git a/AUTHORS b/AUTHORS index 4e4106ac7a..dbeabbd478 100644 --- a/AUTHORS +++ b/AUTHORS @@ -70,5 +70,6 @@ List of contributors, in chronological order: * Gordian Schoenherr (https://github.com/schoenherrg) * Silke Hofstra (https://github.com/silkeh) * Itay Porezky (https://github.com/itayporezky) +* Ales Bregar (https://github.com/abregar) * JupiterRider (https://github.com/JupiterRider) * Agustin Henze (https://github.com/agustinhenze) diff --git a/api/publish.go b/api/publish.go index 1a2b5287bb..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 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, 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 @@ -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/cmd/publish.go b/cmd/publish.go index 4217ff87a6..6f4904ada4 100644 --- a/cmd/publish.go +++ b/cmd/publish.go @@ -1,6 +1,8 @@ package cmd import ( + "strings" + "github.com/aptly-dev/aptly/pgp" "github.com/smira/commander" "github.com/smira/flag" @@ -12,7 +14,20 @@ 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 + } + + 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 +41,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..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.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 (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 7e0d8452ea..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.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 (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 f39269a16d..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.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 (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 6ea638d41f..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.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 (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/docs/Publish.md b/docs/Publish.md index 0077f4d9b6..40cacbfe8b 100644 --- a/docs/Publish.md +++ b/docs/Publish.md @@ -11,7 +11,26 @@ 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 +``` +* 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 diff --git a/pgp/gnupg.go b/pgp/gnupg.go index 3edf121024..bab2db21b2 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,14 @@ func (g *GpgSigner) SetBatch(batch bool) { // SetKey sets key ID to use when signing files func (g *GpgSigner) SetKey(keyRef string) { - g.keyRef = 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 @@ -57,8 +64,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/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: {} 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 da3cd7c6b1..19fd0b52ec 100644 --- a/utils/config_test.go +++ b/utils/config_test.go @@ -102,6 +102,7 @@ func (s *ConfigSuite) TestSaveConfig(c *C) { " \"gpgProvider\": \"gpg\",\n"+ " \"gpgDisableSign\": false,\n"+ " \"gpgDisableVerify\": false,\n"+ + " \"gpgKeys\": null,\n"+ " \"skipContentsPublishing\": false,\n"+ " \"skipBz2Publishing\": false,\n"+ " \"FileSystemPublishEndpoints\": {\n"+ @@ -267,6 +268,7 @@ func (s *ConfigSuite) TestSaveYAML2Config(c *C) { "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"+ @@ -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: