From b659240d191a0924254d95de9ae3963040e0a9d2 Mon Sep 17 00:00:00 2001 From: Alberto Carretero Date: Fri, 12 Jan 2024 16:36:42 +0100 Subject: [PATCH] feat: integrity checks for Ubuntu Release files (#106) This adds the functionalities to add public keys to validate archive InRelease file signatures. Instead of downloading the "Release" files for different suites, download the signed "InRelease" file and validate the signature with the provided key(s). This also extends the chisel release with public keys definitions. They are stored in ASCII armored format in the top-level public-keys property by name (v1-public-keys during the transition). They are then referenced by name in the public-keys list property in archive definitions. --- cmd/chisel/cmd_cut.go | 1 + go.mod | 4 +- go.sum | 44 +-- internal/archive/archive.go | 48 +++- internal/archive/archive_test.go | 112 +++++++- internal/archive/testarchive/testarchive.go | 21 +- internal/pgputil/log.go | 53 ++++ internal/pgputil/openpgp.go | 113 ++++++++ internal/pgputil/openpgp_test.go | 267 ++++++++++++++++++ internal/pgputil/suite_test.go | 25 ++ internal/setup/setup.go | 50 ++-- internal/setup/setup_test.go | 235 ++++++++------- .../setup/testdata/ubuntu-archive-keyring.asc | 157 ---------- .../setup/testdata/ubuntu-master-keyring.asc | 30 -- internal/slicer/slicer_test.go | 17 +- internal/testutil/pgpkeys.go | 182 ++++++++++++ internal/testutil/reindent.go | 23 ++ internal/testutil/reindent_test.go | 38 ++- 18 files changed, 1052 insertions(+), 368 deletions(-) create mode 100644 internal/pgputil/log.go create mode 100644 internal/pgputil/openpgp.go create mode 100644 internal/pgputil/openpgp_test.go create mode 100644 internal/pgputil/suite_test.go delete mode 100644 internal/setup/testdata/ubuntu-archive-keyring.asc delete mode 100644 internal/setup/testdata/ubuntu-master-keyring.asc create mode 100644 internal/testutil/pgpkeys.go diff --git a/cmd/chisel/cmd_cut.go b/cmd/chisel/cmd_cut.go index fd680b6f..e38b1d12 100644 --- a/cmd/chisel/cmd_cut.go +++ b/cmd/chisel/cmd_cut.go @@ -91,6 +91,7 @@ func (cmd *cmdCut) Execute(args []string) error { Suites: archiveInfo.Suites, Components: archiveInfo.Components, CacheDir: cache.DefaultDir("chisel"), + PubKeys: archiveInfo.PubKeys, }) if err != nil { return err diff --git a/go.mod b/go.mod index 18f24d37..823e559c 100644 --- a/go.mod +++ b/go.mod @@ -3,22 +3,20 @@ module github.com/canonical/chisel go 1.20 require ( - github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb github.com/jessevdk/go-flags v1.5.0 github.com/juju/fslock v0.0.0-20160525022230-4d5c94c67b4b github.com/klauspost/compress v1.15.4 github.com/ulikunitz/xz v0.5.10 go.starlark.net v0.0.0-20220328144851-d1966c6b9fcd + golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 golang.org/x/term v0.12.0 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c gopkg.in/yaml.v3 v3.0.0-20220512140231-539c8e751b99 ) require ( - github.com/cloudflare/circl v1.3.3 // indirect github.com/kr/pretty v0.2.1 // indirect github.com/kr/text v0.1.0 // indirect - golang.org/x/crypto v0.7.0 // indirect golang.org/x/sys v0.12.0 // indirect ) diff --git a/go.sum b/go.sum index ee7c440e..029e1f20 100644 --- a/go.sum +++ b/go.sum @@ -1,17 +1,12 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c h1:kMFnB0vCcX7IL/m9Y5LO+KQYv+t1CQOiFe6+SV2J7bE= -github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb h1:m935MPodAbYS46DG4pJSv7WO+VECIWUQ7OJYSoTrMh4= github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb/go.mod h1:PkYb9DJNAwrSvRx5DYA+gUcOIgTGVMNkfSCbZM8cWpI= -github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cloudflare/circl v1.3.3 h1:fE/Qz0QdIGqeWfnwq0RE0R7MI51s0M2E4Ga9kq5AEMs= -github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= @@ -44,72 +39,35 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/ulikunitz/xz v0.5.10 h1:t92gobL9l3HE202wg3rlk19F6X+JOxl9BBrCCMYEYd8= github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.starlark.net v0.0.0-20220328144851-d1966c6b9fcd h1:Uo/x0Ir5vQJ+683GXB9Ug+4fcjsbp7z7Ul8UaZbhsRM= go.starlark.net v0.0.0-20220328144851-d1966c6b9fcd/go.mod h1:t3mmBBPzAVvK0L0n1drDmrQsJ8FoIx4INCqVMTr/Zo0= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= -golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A= -golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= -golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/term v0.12.0 h1:/ZfYdc3zq+q02Rv9vGqTeSItdzZTSNDmfTi0mBAuidU= golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= diff --git a/internal/archive/archive.go b/internal/archive/archive.go index 63badc8f..2a552084 100644 --- a/internal/archive/archive.go +++ b/internal/archive/archive.go @@ -8,9 +8,12 @@ import ( "strings" "time" + "golang.org/x/crypto/openpgp/packet" + "github.com/canonical/chisel/internal/cache" "github.com/canonical/chisel/internal/control" "github.com/canonical/chisel/internal/deb" + "github.com/canonical/chisel/internal/pgputil" ) type Archive interface { @@ -26,6 +29,7 @@ type Options struct { Suites []string Components []string CacheDir string + PubKeys []*packet.PublicKey } func Open(options *Options) (Archive, error) { @@ -64,6 +68,7 @@ type ubuntuArchive struct { options Options indexes []*ubuntuIndex cache *cache.Cache + pubKeys []*packet.PublicKey } type ubuntuIndex struct { @@ -74,7 +79,7 @@ type ubuntuIndex struct { component string release control.Section packages control.File - cache *cache.Cache + archive *ubuntuArchive } func (a *ubuntuArchive) Options() *Options { @@ -140,6 +145,7 @@ func openUbuntu(options *Options) (Archive, error) { cache: &cache.Cache{ Dir: options.CacheDir, }, + pubKeys: options.PubKeys, } for _, suite := range options.Suites { @@ -152,7 +158,7 @@ func openUbuntu(options *Options) (Archive, error) { suite: suite, component: component, release: release, - cache: archive.cache, + archive: archive, } if release == nil { err := index.fetchRelease() @@ -178,20 +184,44 @@ func openUbuntu(options *Options) (Archive, error) { func (index *ubuntuIndex) fetchRelease() error { logf("Fetching %s %s %s suite details...", index.label, index.version, index.suite) - reader, err := index.fetch("Release", "", fetchDefault) + reader, err := index.fetch("InRelease", "", fetchDefault) + if err != nil { + return err + } + defer reader.Close() + + data, err := io.ReadAll(reader) if err != nil { return err } - ctrl, err := control.ParseReader("Label", reader) + // Decode the signature(s) and verify the InRelease file. The InRelease + // file may have multiple signatures from different keys. Verify that at + // least one signature is valid against the archive's set of public keys. + // Unlike gpg --verify which ensures the verification of all signatures, + // this is in line with what apt does internally: + // https://salsa.debian.org/apt-team/apt/-/blob/4e344a4/methods/gpgv.cc#L553-557 + sigs, canonicalBody, err := pgputil.DecodeClearSigned(data) + if err != nil { + return fmt.Errorf("cannot decode clearsigned InRelease file: %v", err) + } + err = pgputil.VerifyAnySignature(index.archive.pubKeys, sigs, canonicalBody) + if err != nil { + return fmt.Errorf("cannot verify signature of the InRelease file") + } + + // canonicalBody has line endings, reverting that to match the + // expected control file format. + body := strings.ReplaceAll(string(canonicalBody), "\r", "") + ctrl, err := control.ParseString("Label", body) if err != nil { - return fmt.Errorf("parsing archive Release file: %v", err) + return fmt.Errorf("cannot parse InRelease file: %v", err) } section := ctrl.Section("Ubuntu") if section == nil { section = ctrl.Section("UbuntuProFIPS") if section == nil { - return fmt.Errorf("corrupted archive Release file: no Ubuntu section") + return fmt.Errorf("corrupted archive InRelease file: no Ubuntu section") } } logf("Release date: %s", section.Get("Date")) @@ -240,7 +270,7 @@ func (index *ubuntuIndex) checkComponents(components []string) error { } func (index *ubuntuIndex) fetch(suffix, digest string, flags fetchFlags) (io.ReadCloser, error) { - reader, err := index.cache.Open(digest) + reader, err := index.archive.cache.Open(digest) if err == nil { return reader, nil } else if err != cache.MissErr { @@ -293,7 +323,7 @@ func (index *ubuntuIndex) fetch(suffix, digest string, flags fetchFlags) (io.Rea body = reader } - writer := index.cache.Create(digest) + writer := index.archive.cache.Create(digest) defer writer.Close() _, err = io.Copy(writer, body) @@ -304,5 +334,5 @@ func (index *ubuntuIndex) fetch(suffix, digest string, flags fetchFlags) (io.Rea return nil, fmt.Errorf("cannot fetch from archive: %v", err) } - return index.cache.Open(writer.Digest()) + return index.archive.cache.Open(writer.Digest()) } diff --git a/internal/archive/archive_test.go b/internal/archive/archive_test.go index cb9f3278..af741dbe 100644 --- a/internal/archive/archive_test.go +++ b/internal/archive/archive_test.go @@ -1,6 +1,7 @@ package archive_test import ( + "golang.org/x/crypto/openpgp/packet" . "gopkg.in/check.v1" "debug/elf" @@ -18,6 +19,7 @@ import ( "github.com/canonical/chisel/internal/archive" "github.com/canonical/chisel/internal/archive/testarchive" "github.com/canonical/chisel/internal/deb" + "github.com/canonical/chisel/internal/testutil" ) type httpSuite struct { @@ -31,10 +33,18 @@ type httpSuite struct { header http.Header status int restore func() + privKey *packet.PrivateKey + pubKey *packet.PublicKey } var _ = Suite(&httpSuite{}) +var ( + key1 = testutil.PGPKeys["key1"] + key2 = testutil.PGPKeys["key2"] + keyUbuntu2018 = testutil.PGPKeys["key-ubuntu-2018"] +) + func (s *httpSuite) SetUpTest(c *C) { s.logf = c.Logf s.err = nil @@ -46,6 +56,8 @@ func (s *httpSuite) SetUpTest(c *C) { s.header = nil s.status = 200 s.restore = archive.FakeDo(s.Do) + s.privKey = key1.PrivKey + s.pubKey = key1.PubKey } func (s *httpSuite) TearDownTest(c *C) { @@ -97,6 +109,7 @@ func (s *httpSuite) prepareArchiveAdjustRelease(suite, version, arch string, com Suite: suite, Version: version, Label: "Ubuntu", + PrivKey: s.privKey, } for i, component := range components { index := &testarchive.PackageIndex{ @@ -172,6 +185,7 @@ func (s *httpSuite) TestOptionErrors(c *C) { cacheDir := c.MkDir() for _, test := range optionErrorTests { test.options.CacheDir = cacheDir + test.options.PubKeys = append(test.options.PubKeys, s.pubKey) _, err := archive.Open(&test.options) c.Assert(err, ErrorMatches, test.error) } @@ -188,6 +202,7 @@ func (s *httpSuite) TestFetchPackage(c *C) { Suites: []string{"jammy"}, Components: []string{"main", "universe"}, CacheDir: c.MkDir(), + PubKeys: []*packet.PublicKey{s.pubKey}, } archive, err := archive.Open(&options) @@ -217,6 +232,7 @@ func (s *httpSuite) TestFetchPortsPackage(c *C) { Suites: []string{"jammy"}, Components: []string{"main", "universe"}, CacheDir: c.MkDir(), + PubKeys: []*packet.PublicKey{s.pubKey}, } archive, err := archive.Open(&options) @@ -254,6 +270,7 @@ func (s *httpSuite) TestFetchSecurityPackage(c *C) { Arch: "amd64", Suites: []string{"jammy", "jammy-security", "jammy-updates"}, Components: []string{"main", "universe"}, + PubKeys: []*packet.PublicKey{s.pubKey}, } archive, err := archive.Open(&options) @@ -284,6 +301,7 @@ func (s *httpSuite) TestArchiveLabels(c *C) { Suites: []string{"jammy"}, Components: []string{"main", "universe"}, CacheDir: c.MkDir(), + PubKeys: []*packet.PublicKey{s.pubKey}, } _, err := archive.Open(&options) @@ -298,6 +316,7 @@ func (s *httpSuite) TestArchiveLabels(c *C) { Suites: []string{"jammy"}, Components: []string{"main", "universe"}, CacheDir: c.MkDir(), + PubKeys: []*packet.PublicKey{s.pubKey}, } _, err = archive.Open(&options) @@ -312,6 +331,7 @@ func (s *httpSuite) TestArchiveLabels(c *C) { Suites: []string{"jammy"}, Components: []string{"main", "universe"}, CacheDir: c.MkDir(), + PubKeys: []*packet.PublicKey{s.pubKey}, } _, err = archive.Open(&options) @@ -326,12 +346,59 @@ func (s *httpSuite) TestArchiveLabels(c *C) { Suites: []string{"jammy"}, Components: []string{"main", "universe"}, CacheDir: c.MkDir(), + PubKeys: []*packet.PublicKey{s.pubKey}, } _, err = archive.Open(&options) c.Assert(err, ErrorMatches, `.*\bno Ubuntu section`) } +type verifyArchiveReleaseTest struct { + summary string + pubKeys []*packet.PublicKey + error string +} + +var verifyArchiveReleaseTests = []verifyArchiveReleaseTest{{ + summary: "A valid public key", + pubKeys: []*packet.PublicKey{key1.PubKey}, +}, { + summary: "No public key to verify with", + error: `cannot verify signature of the InRelease file`, +}, { + summary: "Wrong public key", + pubKeys: []*packet.PublicKey{key2.PubKey}, + error: `cannot verify signature of the InRelease file`, +}, { + summary: "Multiple public keys (invalid, valid)", + pubKeys: []*packet.PublicKey{key2.PubKey, key1.PubKey}, +}} + +func (s *httpSuite) TestVerifyArchiveRelease(c *C) { + for _, test := range verifyArchiveReleaseTests { + c.Logf("Summary: %s", test.summary) + + s.prepareArchive("jammy", "22.04", "amd64", []string{"main", "universe"}) + + options := archive.Options{ + Label: "ubuntu", + Version: "22.04", + Arch: "amd64", + Suites: []string{"jammy"}, + Components: []string{"main", "universe"}, + CacheDir: c.MkDir(), + PubKeys: test.pubKeys, + } + + _, err := archive.Open(&options) + if test.error != "" { + c.Assert(err, ErrorMatches, test.error) + } else { + c.Assert(err, IsNil) + } + } +} + func read(r io.Reader) string { data, err := io.ReadAll(r) if err != nil { @@ -349,11 +416,39 @@ func (s *S) TestRealArchive(c *C) { if !*realArchiveFlag { c.Skip("--real-archive not provided") } - for _, arch := range elfToDebArch { - s.testOpenArchiveArch(c, arch) + for _, release := range ubuntuReleases { + for _, arch := range elfToDebArch { + s.testOpenArchiveArch(c, release, arch) + } } } +type ubuntuRelease struct { + name string + version string + archivePubKeys []*packet.PublicKey +} + +var ubuntuReleases = []ubuntuRelease{{ + name: "focal", + version: "20.04", + archivePubKeys: []*packet.PublicKey{ + keyUbuntu2018.PubKey, + }, +}, { + name: "jammy", + version: "22.04", + archivePubKeys: []*packet.PublicKey{ + keyUbuntu2018.PubKey, + }, +}, { + name: "noble", + version: "24.04", + archivePubKeys: []*packet.PublicKey{ + keyUbuntu2018.PubKey, + }, +}} + var elfToDebArch = map[elf.Machine]string{ elf.EM_386: "i386", elf.EM_AARCH64: "arm64", @@ -373,14 +468,17 @@ func (s *S) checkArchitecture(c *C, arch string, binaryPath string) { c.Assert(binaryArch, Equals, arch) } -func (s *S) testOpenArchiveArch(c *C, arch string) { +func (s *S) testOpenArchiveArch(c *C, release ubuntuRelease, arch string) { + c.Logf("Checking ubuntu archive %s %s...", release.name, arch) + options := archive.Options{ Label: "ubuntu", - Version: "22.04", + Version: release.version, Arch: arch, - Suites: []string{"jammy"}, + Suites: []string{release.name}, Components: []string{"main", "universe"}, CacheDir: c.MkDir(), + PubKeys: release.archivePubKeys, } archive, err := archive.Open(&options) @@ -408,8 +506,8 @@ func (s *S) testOpenArchiveArch(c *C, arch string) { data, err := os.ReadFile(filepath.Join(extractDir, "copyright")) c.Assert(err, IsNil) - copyrightTop := "This package was written by Peter Tobias \non Thu, 16 Jan 1997 01:00:34 +0100." - c.Assert(strings.HasPrefix(string(data), copyrightTop), Equals, true) + copyrightTop := "This package was written by Peter Tobias " + c.Assert(strings.Contains(string(data), copyrightTop), Equals, true) s.checkArchitecture(c, arch, filepath.Join(extractDir, "hostname")) } diff --git a/internal/archive/testarchive/testarchive.go b/internal/archive/testarchive/testarchive.go index dd2ee89f..f51f0977 100644 --- a/internal/archive/testarchive/testarchive.go +++ b/internal/archive/testarchive/testarchive.go @@ -8,6 +8,9 @@ import ( "path" "strings" + "golang.org/x/crypto/openpgp/clearsign" + "golang.org/x/crypto/openpgp/packet" + "github.com/canonical/chisel/internal/testutil" ) @@ -103,6 +106,7 @@ type Release struct { Version string Label string Items []Item + PrivKey *packet.PrivateKey } func (r *Release) Walk(f func(Item) error) error { @@ -110,7 +114,7 @@ func (r *Release) Walk(f func(Item) error) error { } func (r *Release) Path() string { - return "Release" + return "InRelease" } func (r *Release) Section() []byte { @@ -137,7 +141,20 @@ func (r *Release) Content() []byte { %s `)), r.Label, r.Suite, r.Version, r.Version, digests.String()) - return []byte(content) + var buf bytes.Buffer + writer, err := clearsign.Encode(&buf, r.PrivKey, nil) + if err != nil { + panic(err) + } + _, err = writer.Write([]byte(content)) + if err != nil { + panic(err) + } + err = writer.Close() + if err != nil { + panic(err) + } + return buf.Bytes() } func (r *Release) Render(prefix string, content map[string][]byte) error { diff --git a/internal/pgputil/log.go b/internal/pgputil/log.go new file mode 100644 index 00000000..20793699 --- /dev/null +++ b/internal/pgputil/log.go @@ -0,0 +1,53 @@ +package pgputil + +import ( + "fmt" + "sync" +) + +// Avoid importing the log type information unnecessarily. There's a small cost +// associated with using an interface rather than the type. Depending on how +// often the logger is plugged in, it would be worth using the type instead. +type log_Logger interface { + Output(calldepth int, s string) error +} + +var globalLoggerLock sync.Mutex +var globalLogger log_Logger +var globalDebug bool + +// Specify the *log.Logger object where log messages should be sent to. +func SetLogger(logger log_Logger) { + globalLoggerLock.Lock() + globalLogger = logger + globalLoggerLock.Unlock() +} + +// Enable the delivery of debug messages to the logger. Only meaningful +// if a logger is also set. +func SetDebug(debug bool) { + globalLoggerLock.Lock() + globalDebug = debug + globalLoggerLock.Unlock() +} + +// logf sends to the logger registered via SetLogger the string resulting +// from running format and args through Sprintf. +func logf(format string, args ...interface{}) { + globalLoggerLock.Lock() + defer globalLoggerLock.Unlock() + if globalLogger != nil { + globalLogger.Output(2, fmt.Sprintf(format, args...)) + } +} + +// debugf sends to the logger registered via SetLogger the string resulting +// from running format and args through Sprintf, but only if debugging was +// enabled via SetDebug. +func debugf(format string, args ...interface{}) { + globalLoggerLock.Lock() + defer globalLoggerLock.Unlock() + if globalDebug && globalLogger != nil { + globalLogger.Output(2, fmt.Sprintf(format, args...)) + } +} diff --git a/internal/pgputil/openpgp.go b/internal/pgputil/openpgp.go new file mode 100644 index 00000000..dfa013bb --- /dev/null +++ b/internal/pgputil/openpgp.go @@ -0,0 +1,113 @@ +package pgputil + +import ( + "bytes" + "fmt" + "io" + + "golang.org/x/crypto/openpgp/armor" + "golang.org/x/crypto/openpgp/clearsign" + "golang.org/x/crypto/openpgp/packet" +) + +// DecodeKeys decodes public and private key packets from armored data. +func DecodeKeys(armoredData []byte) (pubKeys []*packet.PublicKey, privKeys []*packet.PrivateKey, err error) { + block, err := armor.Decode(bytes.NewReader(armoredData)) + if err != nil { + return nil, nil, fmt.Errorf("cannot decode armored data") + } + + reader := packet.NewReader(block.Body) + for { + p, err := reader.Next() + if err != nil { + if err == io.EOF { + break + } + return nil, nil, err + } + if privKey, ok := p.(*packet.PrivateKey); ok { + privKeys = append(privKeys, privKey) + } + if pubKey, ok := p.(*packet.PublicKey); ok { + pubKeys = append(pubKeys, pubKey) + } + } + return pubKeys, privKeys, nil +} + +// DecodePubKey decodes a single public key packet from armored data. The +// data should contain exactly one public key packet and no private key packets. +func DecodePubKey(armoredData []byte) (*packet.PublicKey, error) { + pubKeys, privKeys, err := DecodeKeys(armoredData) + if err != nil { + return nil, err + } + if len(privKeys) > 0 { + return nil, fmt.Errorf("armored data contains private key") + } + if len(pubKeys) > 1 { + return nil, fmt.Errorf("armored data contains more than one public key") + } + if len(pubKeys) == 0 { + return nil, fmt.Errorf("armored data contains no public key") + } + return pubKeys[0], nil +} + +// DecodeClearSigned decodes the first clearsigned message in the data and +// returns the signatures and the message body. +// +// The returned canonicalBody is canonicalized by converting line endings to +// per the openPGP RCF: https://www.rfc-editor.org/rfc/rfc4880#section-5.2.4 +func DecodeClearSigned(clearData []byte) (sigs []*packet.Signature, canonicalBody []byte, err error) { + block, _ := clearsign.Decode(clearData) + if block == nil { + return nil, nil, fmt.Errorf("cannot decode clearsign text") + } + reader := packet.NewReader(block.ArmoredSignature.Body) + for { + p, err := reader.Next() + if err != nil { + if err == io.EOF { + break + } + return nil, nil, fmt.Errorf("cannot parse armored data: %w", err) + } + if sig, ok := p.(*packet.Signature); ok { + sigs = append(sigs, sig) + } + } + if len(sigs) == 0 { + return nil, nil, fmt.Errorf("clearsigned data contains no signatures") + } + return sigs, block.Bytes, nil +} + +// VerifySignature returns nil if sig is a valid signature from pubKey. +func VerifySignature(pubKey *packet.PublicKey, sig *packet.Signature, body []byte) error { + hash := sig.Hash.New() + _, err := io.Copy(hash, bytes.NewBuffer(body)) + if err != nil { + return err + } + return pubKey.VerifySignature(hash, sig) +} + +// VerifyAnySignature returns nil if any signature in sigs is a valid signature +// mady by any of the public keys in pubKeys. +func VerifyAnySignature(pubKeys []*packet.PublicKey, sigs []*packet.Signature, body []byte) error { + var err error + for _, sig := range sigs { + for _, key := range pubKeys { + err = VerifySignature(key, sig, body) + if err == nil { + return nil + } + } + } + if len(sigs) == 1 && len(pubKeys) == 1 { + return err + } + return fmt.Errorf("cannot verify any signatures") +} diff --git a/internal/pgputil/openpgp_test.go b/internal/pgputil/openpgp_test.go new file mode 100644 index 00000000..cec96300 --- /dev/null +++ b/internal/pgputil/openpgp_test.go @@ -0,0 +1,267 @@ +package pgputil_test + +import ( + "golang.org/x/crypto/openpgp/packet" + . "gopkg.in/check.v1" + + "github.com/canonical/chisel/internal/pgputil" + "github.com/canonical/chisel/internal/testutil" +) + +var ( + key1 = testutil.PGPKeys["key1"] + key2 = testutil.PGPKeys["key2"] +) + +type archiveKeyTest struct { + summary string + armor string + relerror string + pubKey *packet.PublicKey +} + +var archiveKeyTests = []archiveKeyTest{{ + summary: "Armored data with one public key", + armor: key1.PubKeyArmor, + pubKey: key1.PubKey, +}, { + summary: "Armored data with two public keys", + armor: twoPubKeysArmor, + relerror: "armored data contains more than one public key", +}, { + summary: "Armored data with no public key", + armor: armoredDataWithNoKeys, + relerror: "armored data contains no public key", +}, { + summary: "Armored data with private key", + armor: key1.PrivKeyArmor, + relerror: "armored data contains private key", +}, { + summary: "Invalid armored data", + armor: ` + Roses are red + Violets are blue + `, + relerror: "cannot decode armored data", +}, { + summary: "Empty armored data", + relerror: "cannot decode armored data", +}, { + summary: "Armored data: bad packets", + armor: invalidPubKeyArmor, + relerror: "openpgp: .*", +}} + +func (s *S) TestDecodeArchivePubKey(c *C) { + for _, test := range archiveKeyTests { + c.Logf("Summary: %s", test.summary) + + pubKey, err := pgputil.DecodePubKey([]byte(test.armor)) + if test.relerror != "" { + c.Assert(err, ErrorMatches, test.relerror) + continue + } + c.Assert(err, IsNil) + + c.Assert(pubKey, DeepEquals, test.pubKey) + } +} + +type verifyClearSignTest struct { + summary string + clearData string + pubKeys []*packet.PublicKey + relerror string +} + +var verifyClearSignTests = []verifyClearSignTest{{ + summary: "Good data with proper sign", + clearData: clearSignedData, + pubKeys: []*packet.PublicKey{key1.PubKey}, +}, { + summary: "Good data with multiple signatures", + clearData: clearSignedWithMultipleSigns, + pubKeys: []*packet.PublicKey{key1.PubKey, key2.PubKey}, +}, { + summary: "Multiple signatures: verify at least one signature", + clearData: clearSignedWithMultipleSigns, + pubKeys: []*packet.PublicKey{key1.PubKey}, +}, { + summary: "Multiple signatures: no valid public keys", + clearData: clearSignedWithMultipleSigns, + relerror: "cannot verify any signatures", +}, { + summary: "Invalid data: improper hash", + clearData: invalidClearSignedData, + pubKeys: []*packet.PublicKey{key1.PubKey}, + relerror: "openpgp: .*invalid signature: hash tag doesn't match.*", +}, { + summary: "Invalid data: bad packets", + clearData: invalidClearSignedDataBadPackets, + pubKeys: []*packet.PublicKey{key1.PubKey}, + relerror: "cannot parse armored data: openpgp: .*", +}, { + summary: "Invalid data: malformed clearsign text", + clearData: "foo\n", + pubKeys: []*packet.PublicKey{key1.PubKey}, + relerror: "cannot decode clearsign text", +}, { + summary: "Wrong public key to verify with", + clearData: clearSignedData, + pubKeys: []*packet.PublicKey{key2.PubKey}, + relerror: "openpgp: .*invalid signature:.*verification failure", +}} + +func (s *S) TestVerifySignature(c *C) { + for _, test := range verifyClearSignTests { + c.Logf("Summary: %s", test.summary) + + sigs, body, err := pgputil.DecodeClearSigned([]byte(test.clearData)) + if err == nil { + err = pgputil.VerifyAnySignature(test.pubKeys, sigs, body) + } + if test.relerror != "" { + c.Assert(err, ErrorMatches, test.relerror) + continue + } else { + c.Assert(err, IsNil) + } + } +} + +// twoPubKeysArmor contains two public keys: +// - 854BAF1AA9D76600 ("foo-bar ") +// - 871920D1991BC93C ("Ubuntu Archive Automatic Signing Key (2018) ") +const twoPubKeysArmor = ` +-----BEGIN PGP ARMORED FILE----- + +mQENBGVs8P4BCADPh/fNnw2AI1JCYf+3p4jkcFQPLVsUkoTZk8OXjCxy+UP9Jd2m +xnxat7a0JEJZa0aWCmtlSL1XR+kFKBrd7Ry5jOHYjuDKx4kTmDUbezPnjoZIGDNX +j5cdNuMLpOINZweNNWDKRdRvhj5QX89/DYwPrLkNFwwjXjlj5tjU6RUkROYJBGPe +G2ns2cZtVbYMh3FDU9YRfp/hUqGVf+UFRyUw+mo1TUlk5F7fnfwEQmsppDHvfTNJ +yjEMZD7nReTEeMy12GV2wysOwWMPEb2PSE/+Od7AKn5dFA7w3kyLCzAxYp6o7IE/ ++RY8YzAJe6GmLwhTWtylMV1xteQhZkEe/QGXABEBAAGZAg0EW5/B2gEQAO/8bHK3 +H8txJdi4zQzAqiNsHJ7zWzQbWcTH6XPpUBSkhRmduSp1cEcL53CsZL8J54u4CM9E +tTwCjET+OO9lWnzEUYRYdh2SWpcZn+Al8/l3d8hQG1kdkQmXwHyb2kwd/8BBB2wL +5jOLNIbm3kyGei3DTjgte10QSTHa3onPQ4auH7kijGpfulmKroK/X0GiFpSKgox2 +nsRLpFh83uiXodIsWWsxe1V+H+KOk32PdmFUZV5ELyQodCwnk+Qhua/EGJSHtImZ +9lTHQhCE0xoMdd91kAY22eHPM1F5vUWo0tJWStL8+ewBDMyEbUEObZU5IXriN5sp +d98WozktdFBN6pMuyNRtuupHqz8YI7xQXuN9SPoju1ovKCawc78kPiOkpELSBulQ +F9qInIu+56nHeRaiovew3QuGUwjzT58DsZO+g7Hi2mpWXOUTpNqNi76N9bdCk4VL +l7AQx0vbqHPGxmD+B5m9NsCtw/46wkpGaG/iQ2joDJ3IdD/dlX9/df2ZPf9I8tsl +q6aSCndjN3q3k94G75lCT+Y3lY025qKE0RXuWVvVmG9jQXG70FV38E2XSvO7Gnes +iKcHZNfZIKDvATnFeTBe5D/Z5MMTS/QeUae2S5mMajANmTEdlBLFlU7N1kJFVpf9 +YQUukprYBCnDlEmtDihn85+J9fInM/bujTfBABEBAAE= +=Znwy +-----END PGP ARMORED FILE----- +` + +// clearSignedData is signed with key 854BAF1AA9D76600 (foo@bar). +const clearSignedData = ` +-----BEGIN PGP SIGNED MESSAGE----- +Hash: SHA512 + +foo +-----BEGIN PGP SIGNATURE----- + +iQE8BAEBCgAmFiEEDp0LAdsRnT9gfhU5hUuvGqnXZgAFAmVuylQIHGZvb0BiYXIA +CgkQhUuvGqnXZgDB5wf/UaxTLwO22BQdpjtkRWoI9EooNr02K5jW7x4Y73akuBFt +EJi1bUPrNKFqL7VDTMiaRv+1RSytY9U3+AKgMKVq1p7Iwr2t6CLs3D7bqw9Vy2Z4 +SpjS8zZQ5H+7t0O2zqNSu4UqBTCXWIsW9EiL1EHr92F2O3HhOn1ER7KgTl+GDUZ/ +4szrBZsfltvX51UMvFD1TO9EYcJ4tzB6mvftTBZZ6KeoyUC5u4a1ZljYkujWAlFW +VvD4PlSNTcSmpZTICEmLmb3DLlXezQ0Rgfwy6Q6X0kt9xztIJsNo5sgRxQUlpVl3 +5VFsefx4LxtZvdSFK0SNh7UAhdOzD5Tc/7aG0NFfjw== +=BAhz +-----END PGP SIGNATURE----- +` + +// This data contains two signatures, from these keys: +// - 854BAF1AA9D76600 (foo@bar) +// - 9568570379BF1F43 (test@key) +const clearSignedWithMultipleSigns = ` +-----BEGIN PGP SIGNED MESSAGE----- +Hash: SHA512 + +foo +-----BEGIN PGP SIGNATURE----- + +iQEzBAEBCgAdFiEEDp0LAdsRnT9gfhU5hUuvGqnXZgAFAmVxmacACgkQhUuvGqnX +ZgBarggAp4YWGia+9yUJG3ojieaSFnue9Ov/YnhV3PLKqxo1+DJZZDekxuxk7sbU +x+ZQmM/3xus1MhEVmySvwEiuGktr9fk+/eEZOZj6d4ZTTloUeDZNaJ7LSUEUKdMM +HA5Adphtv+vBZwmkH6u7jyJSGC+P/U7DFmIPODeDcqLzh5hjWWK1dkNqkwEF75Ot +9AXI5Y0e4WWJj/UQ1zuUwtw9Rf4JB8MUFOVUPJe4UFZw+XUYHq5DFBNYLn2SDLMQ +BQ3hzmDE9FazILBIFfutKTpA3gmPu9wZ+WroNXkKkleV0Wjo0kA4bnz5hLy2D4Bf +DBATaX5qzUwC9LxpzNJoScsW/2U+KYizBAEBCgAdFiEEVCsygCgPC1R8H4YGlWhX +A3m/H0MFAmVxmacACgkQlWhXA3m/H0PVCgQArXUt7hQO3bATZBsbTgQ2INhs1aiR +GAWkroW5Dp5mOmTtAtfFuysEMdH+v42Z6g1BqwypWtCVNYF+v8aQYwUwUulN/Pna +qtWNWLmXMFLmNVILL9X+o/sRCtra1qCu6Vn59H+yPhye9CXiV+U/V8dB60YLs812 +cgcXWByCFx3J1hM= +=1GLl +-----END PGP SIGNATURE----- +` + +// This should be an invalid clearsign data. Obtained by changing +// "foo" to "bar" in clearSignedData defined above. +const invalidClearSignedData = ` +-----BEGIN PGP SIGNED MESSAGE----- +Hash: SHA512 + +bar +-----BEGIN PGP SIGNATURE----- + +iQE8BAEBCgAmFiEEDp0LAdsRnT9gfhU5hUuvGqnXZgAFAmVuylQIHGZvb0BiYXIA +CgkQhUuvGqnXZgDB5wf/UaxTLwO22BQdpjtkRWoI9EooNr02K5jW7x4Y73akuBFt +EJi1bUPrNKFqL7VDTMiaRv+1RSytY9U3+AKgMKVq1p7Iwr2t6CLs3D7bqw9Vy2Z4 +SpjS8zZQ5H+7t0O2zqNSu4UqBTCXWIsW9EiL1EHr92F2O3HhOn1ER7KgTl+GDUZ/ +4szrBZsfltvX51UMvFD1TO9EYcJ4tzB6mvftTBZZ6KeoyUC5u4a1ZljYkujWAlFW +VvD4PlSNTcSmpZTICEmLmb3DLlXezQ0Rgfwy6Q6X0kt9xztIJsNo5sgRxQUlpVl3 +5VFsefx4LxtZvdSFK0SNh7UAhdOzD5Tc/7aG0NFfjw== +=BAhz +-----END PGP SIGNATURE----- +` + +// This should be an invalid clearsign data with invalid packets. +// Obtained by removing some lines from clearSignedData above. +const invalidClearSignedDataBadPackets = ` +-----BEGIN PGP SIGNED MESSAGE----- +Hash: SHA512 + +foo +-----BEGIN PGP SIGNATURE----- + +qtWNWLmXMFLmNVILL9X+o/sRCtra1qCu6Vn59H+yPhye9CXiV+U/V8dB60YLs812 +cgcXWByCFx3J1hM= +=1GLl +-----END PGP SIGNATURE----- +` + +// armoredDataWithNoKeys contains only a signature packet, to be +// used for testing purposes. It does not contain any key packets. +const armoredDataWithNoKeys = ` +-----BEGIN PGP ARMORED FILE----- +Comment: Use "gpg --dearmor" for unpacking + +iQI4BBMBCgAiBQJbn8HaAhsDBgsJCAcDAgYVCAIJCgsEFgIDAQIeAQIXgAAKCRCH +GSDRmRvJPCxzEACktnJ8c/+VmqAjlgK3+YOlB23jgoHOQwZtIQrhQ2Vlr+Nu2hno +twj7i8NAxiwl2XcnOXahPJr4zJTppgCipY9bhoN02Am0Fo1j3jJwT2W5BYJGaFye +/+gge21kYbdbB86bdS02fkmA8DsCevEEaew0WmZfWOkIlG3roatg1HE6H1WwcW4a +3JDeGbXi75vv5xvZv3IqKXOui8EXZManyd9gsqvtU0uVWiCQxuw1s4hvim7uqggz +OEDZYNyx+6deAq0cQG3OJb6IUYLFeHkKrCHHRZLlWORzz49ivE6qWOkk3vBodGqa +xtUVfGSmstykjKZ8ldXwCp+HzPW8oi80AKLwtC2fTDDLKwEv+OQLwtyBCkkoYyxZ +9V9XUQojuv+45mRKGbQKed4ZH/EjAbIu/IVTawbpmcHyHQQNb9tvi2OMUCvKuFwq +EXAPRvqb81PWFVu3EZw2WRpdLsDsO8/T5EAReShSo1g8+HwpPiuvmLRqaLxinpBg +W/COxAOlKbz4KgP0HSNLdSAT9DdOkUHLNX1GgEBLc+gxsuc5EYUeKRkmZ/nRRE+z +3QIxCvOMuwXWOLflNY3EiLwY9Bdgey8ES+8RqUqSCov3pAFy7Nde27xR2gr5lGDe +yVadRjJlRcYSHceghZt38RvEIzW+bXq3v2KivrjoHF58tVJcLQlM5a0mjw== +=cp5f +-----END PGP ARMORED FILE----- +` + +// invalidPubKeyArmor contains bad packets. +const invalidPubKeyArmor = ` +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mI0EZXAwcgEEAMBQ4Qx6xam1k1hyjPrKQfCnGRBBm2+Lw9DHQcz0lreH51iZEVkS +=U79/ +-----END PGP PUBLIC KEY BLOCK----- +` diff --git a/internal/pgputil/suite_test.go b/internal/pgputil/suite_test.go new file mode 100644 index 00000000..1fa96dbd --- /dev/null +++ b/internal/pgputil/suite_test.go @@ -0,0 +1,25 @@ +package pgputil_test + +import ( + "testing" + + . "gopkg.in/check.v1" + + "github.com/canonical/chisel/internal/pgputil" +) + +func Test(t *testing.T) { TestingT(t) } + +type S struct{} + +var _ = Suite(&S{}) + +func (s *S) SetUpTest(c *C) { + pgputil.SetDebug(true) + pgputil.SetLogger(c) +} + +func (s *S) TearDownTest(c *C) { + pgputil.SetDebug(false) + pgputil.SetLogger(nil) +} diff --git a/internal/setup/setup.go b/internal/setup/setup.go index 7840d406..e04f8eca 100644 --- a/internal/setup/setup.go +++ b/internal/setup/setup.go @@ -9,10 +9,11 @@ import ( "regexp" "strings" - "github.com/ProtonMail/go-crypto/openpgp" + "golang.org/x/crypto/openpgp/packet" "gopkg.in/yaml.v3" "github.com/canonical/chisel/internal/deb" + "github.com/canonical/chisel/internal/pgputil" "github.com/canonical/chisel/internal/strdist" ) @@ -31,7 +32,7 @@ type Archive struct { Version string Suites []string Components []string - Keyrings []openpgp.KeyRing + PubKeys []*packet.PublicKey } // Package holds a collection of slices that represent parts of themselves. @@ -320,7 +321,7 @@ func readSlices(release *Release, baseDir, dirName string) error { type yamlRelease struct { Format string `yaml:"format"` Archives map[string]yamlArchive `yaml:"archives"` - Keyrings map[string]string `yaml:"public-keys"` + PubKeys map[string]yamlPubKey `yaml:"v1-public-keys"` } const yamlReleaseFormat = "chisel-v1" @@ -330,7 +331,7 @@ type yamlArchive struct { Suites []string `yaml:"suites"` Components []string `yaml:"components"` Default bool `yaml:"default"` - Keyrings []string `yaml:"public-keys"` + PubKeys []string `yaml:"v1-public-keys"` } type yamlPackage struct { @@ -388,6 +389,11 @@ type yamlSlice struct { Mutate string `yaml:"mutate"` } +type yamlPubKey struct { + ID string `yaml:"id"` + Armor string `yaml:"armor"` +} + var ubuntuAdjectives = map[string]string{ "18.04": "bionic", "20.04": "focal", @@ -418,14 +424,17 @@ func parseRelease(baseDir, filePath string, data []byte) (*Release, error) { return nil, fmt.Errorf("%s: no archives defined", fileName) } - keyringsByName := make(map[string]openpgp.KeyRing, 0) - - for keyringName, keyringArmored := range yamlVar.Keyrings { - keyring, err := openpgp.ReadArmoredKeyRing(strings.NewReader(keyringArmored)) + // Decode the public keys and match against provided IDs. + pubKeys := make(map[string]*packet.PublicKey, len(yamlVar.PubKeys)) + for keyName, yamlPubKey := range yamlVar.PubKeys { + key, err := pgputil.DecodePubKey([]byte(yamlPubKey.Armor)) if err != nil { - return nil, fmt.Errorf("%s: cannot parse keyring %q: %w", fileName, keyringName, err) + return nil, fmt.Errorf("%s: cannot decode public key %q: %w", fileName, keyName, err) } - keyringsByName[keyringName] = keyring + if yamlPubKey.ID != key.KeyIdString() { + return nil, fmt.Errorf("%s: public key %q armor has incorrect ID: expected %q, got %q", fileName, keyName, yamlPubKey.ID, key.KeyIdString()) + } + pubKeys[keyName] = key } for archiveName, details := range yamlVar.Archives { @@ -442,14 +451,6 @@ func parseRelease(baseDir, filePath string, data []byte) (*Release, error) { if len(details.Components) == 0 { return nil, fmt.Errorf("%s: archive %q missing components field", fileName, archiveName) } - var archiveKeyrings []openpgp.KeyRing - for _, keyringName := range details.Keyrings { - keyring := keyringsByName[keyringName] - if keyring == nil { - return nil, fmt.Errorf("%s: archive %q references unknown keyring %q", fileName, archiveName, keyringName) - } - archiveKeyrings = append(archiveKeyrings, keyring) - } if len(yamlVar.Archives) == 1 { details.Default = true } else if details.Default && release.DefaultArchive != "" { @@ -458,12 +459,23 @@ func parseRelease(baseDir, filePath string, data []byte) (*Release, error) { if details.Default { release.DefaultArchive = archiveName } + if len(details.PubKeys) == 0 { + return nil, fmt.Errorf("%s: archive %q missing v1-public-keys field", fileName, archiveName) + } + var archiveKeys []*packet.PublicKey + for _, keyName := range details.PubKeys { + key, ok := pubKeys[keyName] + if !ok { + return nil, fmt.Errorf("%s: archive %q refers to undefined public key %q", fileName, archiveName, keyName) + } + archiveKeys = append(archiveKeys, key) + } release.Archives[archiveName] = &Archive{ Name: archiveName, Version: details.Version, Suites: details.Suites, Components: details.Components, - Keyrings: archiveKeyrings, + PubKeys: archiveKeys, } } diff --git a/internal/setup/setup_test.go b/internal/setup/setup_test.go index 81decfbb..9bbf444b 100644 --- a/internal/setup/setup_test.go +++ b/internal/setup/setup_test.go @@ -1,43 +1,19 @@ package setup_test import ( - _ "embed" "os" "path/filepath" - "strings" - "github.com/ProtonMail/go-crypto/openpgp" + "golang.org/x/crypto/openpgp/packet" . "gopkg.in/check.v1" "github.com/canonical/chisel/internal/setup" "github.com/canonical/chisel/internal/testutil" ) -func parseKeyring(ascii string) openpgp.KeyRing { - keyring, err := openpgp.ReadArmoredKeyRing(strings.NewReader(ascii)) - if err != nil { - panic(err) - } - return keyring -} - -func indentLines(text string, indent string) string { - var result strings.Builder - for _, line := range strings.Split(text, "\n") { - result.WriteString(indent) - result.WriteString(line) - result.WriteByte('\n') - } - return result.String() -} - var ( - //go:embed testdata/ubuntu-archive-keyring.asc - testKeyringUbuntuArchiveASCII string - testKeyringUbuntuArchive = parseKeyring(testKeyringUbuntuArchiveASCII) - //go:embed testdata/ubuntu-master-keyring.asc - testKeyringUbuntuMasterASCII string - testKeyringUbuntuMaster = parseKeyring(testKeyringUbuntuMasterASCII) + testKey = testutil.PGPKeys["key1"] + extraTestKey = testutil.PGPKeys["key2"] ) type setupTest struct { @@ -84,6 +60,11 @@ var setupTests = []setupTest{{ version: 22.04 components: [main, other] suites: [jammy, jammy-security] + v1-public-keys: [test-key] + v1-public-keys: + test-key: + id: ` + testKey.ID + ` + armor: |` + "\n" + testutil.PrefixEachLine(testKey.PubKeyArmor, "\t\t\t\t\t\t") + ` `, "slices/mydir/mypkg.yaml": ` package: mypkg @@ -98,6 +79,7 @@ var setupTests = []setupTest{{ Version: "22.04", Suites: []string{"jammy", "jammy-security"}, Components: []string{"main", "other"}, + PubKeys: []*packet.PublicKey{testKey.PubKey}, }, }, Packages: map[string]*setup.Package{ @@ -141,6 +123,7 @@ var setupTests = []setupTest{{ Version: "22.04", Suites: []string{"jammy"}, Components: []string{"main", "universe"}, + PubKeys: []*packet.PublicKey{testKey.PubKey}, }, }, Packages: map[string]*setup.Package{ @@ -202,6 +185,7 @@ var setupTests = []setupTest{{ Version: "22.04", Suites: []string{"jammy"}, Components: []string{"main", "universe"}, + PubKeys: []*packet.PublicKey{testKey.PubKey}, }, }, Packages: map[string]*setup.Package{ @@ -462,6 +446,7 @@ var setupTests = []setupTest{{ Version: "22.04", Suites: []string{"jammy"}, Components: []string{"main", "universe"}, + PubKeys: []*packet.PublicKey{testKey.PubKey}, }, }, Packages: map[string]*setup.Package{ @@ -676,6 +661,7 @@ var setupTests = []setupTest{{ Version: "22.04", Suites: []string{"jammy"}, Components: []string{"main", "universe"}, + PubKeys: []*packet.PublicKey{testKey.PubKey}, }, }, Packages: map[string]*setup.Package{ @@ -715,6 +701,7 @@ var setupTests = []setupTest{{ Version: "22.04", Suites: []string{"jammy"}, Components: []string{"main", "universe"}, + PubKeys: []*packet.PublicKey{testKey.PubKey}, }, }, Packages: map[string]*setup.Package{ @@ -755,6 +742,7 @@ var setupTests = []setupTest{{ Version: "22.04", Suites: []string{"jammy"}, Components: []string{"main", "universe"}, + PubKeys: []*packet.PublicKey{testKey.PubKey}, }, }, Packages: map[string]*setup.Package{ @@ -786,10 +774,16 @@ var setupTests = []setupTest{{ components: [main, universe] suites: [jammy] default: true + v1-public-keys: [test-key] bar: version: 22.04 components: [universe] suites: [jammy-updates] + v1-public-keys: [test-key] + v1-public-keys: + test-key: + id: ` + testKey.ID + ` + armor: |` + "\n" + testutil.PrefixEachLine(testKey.PubKeyArmor, "\t\t\t\t\t\t") + ` `, "slices/mydir/mypkg.yaml": ` package: mypkg @@ -804,12 +798,14 @@ var setupTests = []setupTest{{ Version: "22.04", Suites: []string{"jammy"}, Components: []string{"main", "universe"}, + PubKeys: []*packet.PublicKey{testKey.PubKey}, }, "bar": { Name: "bar", Version: "22.04", Suites: []string{"jammy-updates"}, Components: []string{"universe"}, + PubKeys: []*packet.PublicKey{testKey.PubKey}, }, }, Packages: map[string]*setup.Package{ @@ -822,7 +818,65 @@ var setupTests = []setupTest{{ }, }, }, { - summary: "Archives with keyrings", + summary: "Extra fields in YAML are ignored (necessary for forward compatibility)", + input: map[string]string{ + "chisel.yaml": ` + format: chisel-v1 + archives: + ubuntu: + version: 22.04 + components: [main, other] + suites: [jammy, jammy-security] + v1-public-keys: [test-key] + madeUpKey1: whatever + madeUpKey2: whatever + v1-public-keys: + test-key: + id: ` + testKey.ID + ` + armor: |` + "\n" + testutil.PrefixEachLine(testKey.PubKeyArmor, "\t\t\t\t\t\t") + ` + madeUpKey6: whatever + `, + "slices/mydir/mypkg.yaml": ` + package: mypkg + madeUpKey3: whatever + slices: + myslice: + madeUpKey4: whatever + contents: + /path: {madeUpKey5: whatever} + `, + }, + release: &setup.Release{ + DefaultArchive: "ubuntu", + + Archives: map[string]*setup.Archive{ + "ubuntu": { + Name: "ubuntu", + Version: "22.04", + Suites: []string{"jammy", "jammy-security"}, + Components: []string{"main", "other"}, + PubKeys: []*packet.PublicKey{testKey.PubKey}, + }, + }, + Packages: map[string]*setup.Package{ + "mypkg": { + Archive: "ubuntu", + Name: "mypkg", + Path: "slices/mydir/mypkg.yaml", + Slices: map[string]*setup.Slice{ + "myslice": { + Package: "mypkg", + Name: "myslice", + Contents: map[string]setup.PathInfo{ + "/path": {Kind: "copy"}, + }, + }, + }, + }, + }, + }, +}, { + summary: "Archives with public keys", input: map[string]string{ "chisel.yaml": ` format: chisel-v1 @@ -831,16 +885,20 @@ var setupTests = []setupTest{{ version: 22.04 components: [main, universe] suites: [jammy] - public-keys: [ubuntu-archive] + v1-public-keys: [extra-key] default: true bar: version: 22.04 components: [universe] suites: [jammy-updates] - public-keys: [ubuntu-archive, ubuntu-master] - public-keys: - ubuntu-archive: |` + "\n" + indentLines(testKeyringUbuntuArchiveASCII, "\t\t\t\t\t") + ` - ubuntu-master: |` + "\n" + indentLines(testKeyringUbuntuMasterASCII, "\t\t\t\t\t") + ` + v1-public-keys: [test-key, extra-key] + v1-public-keys: + extra-key: + id: ` + extraTestKey.ID + ` + armor: |` + "\n" + testutil.PrefixEachLine(extraTestKey.PubKeyArmor, "\t\t\t\t\t\t") + ` + test-key: + id: ` + testKey.ID + ` + armor: |` + "\n" + testutil.PrefixEachLine(testKey.PubKeyArmor, "\t\t\t\t\t\t") + ` `, "slices/mydir/mypkg.yaml": ` package: mypkg @@ -855,14 +913,14 @@ var setupTests = []setupTest{{ Version: "22.04", Suites: []string{"jammy"}, Components: []string{"main", "universe"}, - Keyrings: []openpgp.KeyRing{testKeyringUbuntuArchive}, + PubKeys: []*packet.PublicKey{extraTestKey.PubKey}, }, "bar": { Name: "bar", Version: "22.04", Suites: []string{"jammy-updates"}, Components: []string{"universe"}, - Keyrings: []openpgp.KeyRing{testKeyringUbuntuArchive, testKeyringUbuntuMaster}, + PubKeys: []*packet.PublicKey{testKey.PubKey, extraTestKey.PubKey}, }, }, Packages: map[string]*setup.Package{ @@ -875,7 +933,7 @@ var setupTests = []setupTest{{ }, }, }, { - summary: "Unknown keyring", + summary: "Archive without public keys", input: map[string]string{ "chisel.yaml": ` format: chisel-v1 @@ -884,23 +942,30 @@ var setupTests = []setupTest{{ version: 22.04 components: [main, universe] suites: [jammy] - public-keys: [ubuntu-archive] default: true - bar: + `, + }, + relerror: `chisel.yaml: archive "foo" missing v1-public-keys field`, +}, { + summary: "Unknown public key", + input: map[string]string{ + "chisel.yaml": ` + format: chisel-v1 + archives: + foo: version: 22.04 - components: [universe] - suites: [jammy-updates] - public-keys: [ubuntu-master] - public-keys: - ubuntu-master: |` + "\n" + indentLines(testKeyringUbuntuMasterASCII, "\t\t\t\t\t") + ` + components: [main, universe] + suites: [jammy] + v1-public-keys: [extra-key] + default: true `, "slices/mydir/mypkg.yaml": ` package: mypkg `, }, - relerror: `chisel.yaml: archive "foo" references unknown keyring "ubuntu-archive"`, + relerror: `chisel.yaml: archive "foo" refers to undefined public key "extra-key"`, }, { - summary: "Invalid keyring", + summary: "Invalid public key", input: map[string]string{ "chisel.yaml": ` format: chisel-v1 @@ -909,81 +974,59 @@ var setupTests = []setupTest{{ version: 22.04 components: [main, universe] suites: [jammy] - public-keys: [ubuntu-master] + v1-public-keys: [extra-key] default: true - public-keys: - ubuntu-master: | - G. B. Shaw's Law: - Those who can -- do. - Those who can't -- teach. - - Martin's Extension: - Those who cannot teach -- administrate. + v1-public-keys: + extra-key: + id: foo + armor: | + G. B. Shaw's Law: + Those who can -- do. + Those who can't -- teach. + + Martin's Extension: + Those who cannot teach -- administrate. `, "slices/mydir/mypkg.yaml": ` package: mypkg `, }, - relerror: `chisel.yaml: cannot parse keyring "ubuntu-master": openpgp: invalid argument: no armored data found`, + relerror: `chisel.yaml: cannot decode public key "extra-key": cannot decode armored data`, }, { - summary: "Extra fields in YAML are ignored (necessary for forward compatibility)", + summary: "Mismatched public key ID", input: map[string]string{ "chisel.yaml": ` format: chisel-v1 archives: - ubuntu: + foo: version: 22.04 - components: [main, other] - suites: [jammy, jammy-security] - madeUpKey1: whatever - madeUpKey2: whatever + components: [main, universe] + suites: [jammy] + v1-public-keys: [extra-key] + default: true + v1-public-keys: + extra-key: + id: ` + extraTestKey.ID + ` + armor: |` + "\n" + testutil.PrefixEachLine(testKey.PubKeyArmor, "\t\t\t\t\t\t") + ` `, "slices/mydir/mypkg.yaml": ` package: mypkg - madeUpKey3: whatever - slices: - myslice: - madeUpKey4: whatever - contents: - /path: {madeUpKey5: whatever} `, }, - release: &setup.Release{ - DefaultArchive: "ubuntu", - - Archives: map[string]*setup.Archive{ - "ubuntu": { - Name: "ubuntu", - Version: "22.04", - Suites: []string{"jammy", "jammy-security"}, - Components: []string{"main", "other"}, - }, - }, - Packages: map[string]*setup.Package{ - "mypkg": { - Archive: "ubuntu", - Name: "mypkg", - Path: "slices/mydir/mypkg.yaml", - Slices: map[string]*setup.Slice{ - "myslice": { - Package: "mypkg", - Name: "myslice", - Contents: map[string]setup.PathInfo{ - "/path": {Kind: "copy"}, - }, - }, - }, - }, - }, - }, + relerror: `chisel.yaml: public key "extra-key" armor has incorrect ID: expected "9568570379BF1F43", got "854BAF1AA9D76600"`, }} -const defaultChiselYaml = ` +var defaultChiselYaml = ` format: chisel-v1 archives: ubuntu: version: 22.04 components: [main, universe] + v1-public-keys: [test-key] + v1-public-keys: + test-key: + id: ` + testKey.ID + ` + armor: |` + "\n" + testutil.PrefixEachLine(testKey.PubKeyArmor, "\t\t\t\t\t\t") + ` ` func (s *S) TestParseRelease(c *C) { diff --git a/internal/setup/testdata/ubuntu-archive-keyring.asc b/internal/setup/testdata/ubuntu-archive-keyring.asc deleted file mode 100644 index b185006e..00000000 --- a/internal/setup/testdata/ubuntu-archive-keyring.asc +++ /dev/null @@ -1,157 +0,0 @@ ------BEGIN PGP PUBLIC KEY BLOCK----- - -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 -D/rGW6BVDiRZbn6ZT5SJQX16fbwJ5/ksKht2MgjmVIqMi5PD6dBDtUUbavYaoC+Q -6Zh3xjTuSGO2DBfodIa/xDL9SNz+LqSbN0UR5Q4I/7KHtg1ir09HYUPEjy+ZloBB -a7Zya+lgBogQS599cAhs3RE7YBB8R5kCDQRbn8HaARAA7/xscrcfy3El2LjNDMCq -I2wcnvNbNBtZxMfpc+lQFKSFGZ25KnVwRwvncKxkvwnni7gIz0S1PAKMRP4472Va -fMRRhFh2HZJalxmf4CXz+Xd3yFAbWR2RCZfAfJvaTB3/wEEHbAvmM4s0hubeTIZ6 -LcNOOC17XRBJMdreic9Dhq4fuSKMal+6WYqugr9fQaIWlIqCjHaexEukWHze6Jeh -0ixZazF7VX4f4o6TfY92YVRlXkQvJCh0LCeT5CG5r8QYlIe0iZn2VMdCEITTGgx1 -33WQBjbZ4c8zUXm9RajS0lZK0vz57AEMzIRtQQ5tlTkheuI3myl33xajOS10UE3q -ky7I1G266kerPxgjvFBe431I+iO7Wi8oJrBzvyQ+I6SkQtIG6VAX2oici77nqcd5 -FqKi97DdC4ZTCPNPnwOxk76DseLaalZc5ROk2o2Lvo31t0KThUuXsBDHS9uoc8bG -YP4Hmb02wK3D/jrCSkZob+JDaOgMnch0P92Vf391/Zk9/0jy2yWrppIKd2M3ereT -3gbvmUJP5jeVjTbmooTRFe5ZW9WYb2NBcbvQVXfwTZdK87sad6yIpwdk19kgoO8B -OcV5MF7kP9nkwxNL9B5Rp7ZLmYxqMA2ZMR2UEsWVTs3WQkVWl/1hBS6SmtgEKcOU -Sa0OKGfzn4n18icz9u6NN8EAEQEAAbRCVWJ1bnR1IEFyY2hpdmUgQXV0b21hdGlj -IFNpZ25pbmcgS2V5ICgyMDE4KSA8ZnRwbWFzdGVyQHVidW50dS5jb20+iQI4BBMB -CgAiBQJbn8HaAhsDBgsJCAcDAgYVCAIJCgsEFgIDAQIeAQIXgAAKCRCHGSDRmRvJ -PCxzEACktnJ8c/+VmqAjlgK3+YOlB23jgoHOQwZtIQrhQ2Vlr+Nu2hnotwj7i8NA -xiwl2XcnOXahPJr4zJTppgCipY9bhoN02Am0Fo1j3jJwT2W5BYJGaFye/+gge21k -YbdbB86bdS02fkmA8DsCevEEaew0WmZfWOkIlG3roatg1HE6H1WwcW4a3JDeGbXi -75vv5xvZv3IqKXOui8EXZManyd9gsqvtU0uVWiCQxuw1s4hvim7uqggzOEDZYNyx -+6deAq0cQG3OJb6IUYLFeHkKrCHHRZLlWORzz49ivE6qWOkk3vBodGqaxtUVfGSm -stykjKZ8ldXwCp+HzPW8oi80AKLwtC2fTDDLKwEv+OQLwtyBCkkoYyxZ9V9XUQoj -uv+45mRKGbQKed4ZH/EjAbIu/IVTawbpmcHyHQQNb9tvi2OMUCvKuFwqEXAPRvqb -81PWFVu3EZw2WRpdLsDsO8/T5EAReShSo1g8+HwpPiuvmLRqaLxinpBgW/COxAOl -Kbz4KgP0HSNLdSAT9DdOkUHLNX1GgEBLc+gxsuc5EYUeKRkmZ/nRRE+z3QIxCvOM -uwXWOLflNY3EiLwY9Bdgey8ES+8RqUqSCov3pAFy7Nde27xR2gr5lGDeyVadRjJl -RcYSHceghZt38RvEIzW+bXq3v2KivrjoHF58tVJcLQlM5a0mj4kCMwQQAQoAHRYh -BBU/HJ7xOV+/ADUujQv7hH8/Jy9bBQJbn8RDAAoJEAv7hH8/Jy9bbhcP/RoGnoIL -wp9eUKZQAWvOjkXiQEcZwMaWi9tt6S5IAGwWADk+z5k48MBwqhniWRi8wELBi3Ol -pEA3oHsEAjFi6ftczh5lAR22T7M9xO+gHN/NRQF4WQY/DC23MjkTrCmCmfTP8hnq -zKVceAfFjW+T/rfbbQMMAEf5TbOTkt5aVeJ5MCM78QOlp6tIFigS//a3O7C/qlni -Q50BJKtWf3TQW4CFpLQ7aniFxZXYI2Dl/sdUTfNW3i1Q7US6DlNCJELBRmjjm9KN -sfP3ZmDNnF7nITRmJnWNmeY3iyNRdHcwkfgkVBAxXa9HBfeFEoFRlsgqGh3QAU0Q -+Xv7iBMki9E/cpvd0TQbaHPYDxDRQdgEjCYJDDSDYlfNmpT42GK27PmVR7i0CIHf -qsPzes8C7VQ4KNj3OhV2aapko0UZrQUSbr/lZwwXgDrLZdEJaEZuYEQaf8ILfdxN -QIfkCUVbjEBas9Jz2Vk8H3BmoJkhLq1oil/J9hRWJIi38lFtN9+UzfPGfffoL0Pg -MkgBbvEXk/5UMwD0TzUS46QJOXtRbjM0GKASXGMD9LIwCDtQFpoLjyNSi351Z157 -E+SWDGxtgwixyPziL56UavL/eeYJWeS/WqvGzZzsAtgSujFVLKWyUaRi0NvYW3h/ -I50Tzj0Pkm8GtgvP2UqAWvy+iRpeUQ2ji0Nc -=j6+P ------END PGP PUBLIC KEY BLOCK----- diff --git a/internal/setup/testdata/ubuntu-master-keyring.asc b/internal/setup/testdata/ubuntu-master-keyring.asc deleted file mode 100644 index ec6fd997..00000000 --- a/internal/setup/testdata/ubuntu-master-keyring.asc +++ /dev/null @@ -1,30 +0,0 @@ ------BEGIN PGP PUBLIC KEY BLOCK----- - -mQINBEc0weABEACyhLpI9Z2tvMhgV6AywGNKKLbeJE1Gaj1stn6t7ApNf4Q46FUi -D6WJ8voPiSqLdV3xaGLOEi+qbpIUOzGUNPNt+D0tMj72LCK3IIMUR2PnVyr+yNFa -8nB4ymArrCZERIpB75cOF3MUDMcA85AXoHd6+TjEqc3yP3tg0pUmGWrKHXP4TOSg -/cp1s67kLOk/qOjdilFAdIU02BP/XmIDxYrlzH97x/qQVJTzlBihPB4QllIp2kX6 -VD9wEA9j/n9+vXQC4B17h5NRucugtfA8Ud1016GJUfIqWdTgwU/FBx7T6XbS9mCh -6ewD7d58Yndtoo8+N/LjBMh83WrjhvO5DCAcrJL2QCJZRihaQOCPCf+iMicel8n7 -AhJ+ty/ppCLEd6PAUspPBMLVx1yKHLDc/WsG4QWiwAGrWndHDy0ZBouUfCWDNUws -FpB+/BG8mKbTRhAAtQyIdCekXfr7YndPTuL9gB9+qCSn58LCIZU68SUBv80BLpM1 -4iIP/vwZQoVVeKESqDjgpTfqitKfFjOZ+Nd3DylW3pPTDZHNXClDemPucr8ur2MA -QaYjPlsT8eMch82+eaeLDw2xFokpHAVQ3pprRNldJA03jmxvcIPfijuCW1Y7Ivg1 -wincXKPwPgIZWP/NphuqgOid8aoNBtenVrc39gMT7N2D3kiOrixnM3QxtQARAQAB -tDhVYnVudHUgQXJjaGl2ZSBNYXN0ZXIgU2lnbmluZyBLZXkgPGZ0cG1hc3RlckB1 -YnVudHUuY29tPokCNgQTAQIAIAUCRzTB4AIbAwYLCQgHAwIEFQIIAwQWAgMBAh4B -AheAAAoJEAv7hH8/Jy9bGPIQAJ/QYTiOQjUsjqA0bVIaqDOd96w/S746lKthHGpO -Tt1xyi4Lh9CVAjWiqZNE0oKVQFn98Qiswn7M6RlCVEyu4lZRZvUcntn+IV5KxRBy -7zUfHLx4wjMoLaR0N5ahAC5W7aSpQOc6wtMRw3vfxYaF7/oFabZ46b9QryC6yVVm -S5mQrRmNLA6Gg7eNR4TioZKnfDaTl5masOTrVaRlOkAQZ1YpsKeq5EsvXG17rp0C -6FlroINLgU2jvGE86OOv/YatNMajBHaK4cHzd9IPMPygSeZoN0V3n3RzqDpUUE4A -cRXc5gydPbyuuk4YrepnY65VB1KRXyUjO7gXnaWoAfqkJ2dUoO/5jRSoHED3tqeU -1NJRmTQX8xWq126xNcLw3MrjVopbqO85fIwtHF3xONDZwgjHBwqd4DN1dPBBo/rh -m/U9WOPW3vlQXmCwawsVaax11jFqyQX1+86yY6+XOGkSZDrXEJTPnG0C1feHHqCj -7wnuDEGkOFhjFpKr4YOlI/p45GhHnzYLwbK3OA0qYNN17OmRInt7WRNtDK2d+lIN -uep0Y1NZuwVC+4dwqbuuzJLyh3bP2Mg2lADWN5P6bn+e3KiEEJxppTT4k+H9RRUX -j48csEjv6COLs/dvoZlcrzHV7rzxWGSOJh70nphydKKAdbgD7Kdx4d6T+OfgACJW -0SyyiEYEEBECAAYFAkeN/lMACgkQ9t0zAhD6TNHSwQCgi7eTe4D8OXYiUbRnRCiV -Qw3k5+kAn3ho3p27J8RjLNcfpfH6fGfuG2Rh -=+CyM ------END PGP PUBLIC KEY BLOCK----- diff --git a/internal/slicer/slicer_test.go b/internal/slicer/slicer_test.go index aa9d0f51..48e21bf1 100644 --- a/internal/slicer/slicer_test.go +++ b/internal/slicer/slicer_test.go @@ -16,6 +16,10 @@ import ( "github.com/canonical/chisel/internal/testutil" ) +var ( + testKey = testutil.PGPKeys["key1"] +) + type slicerTest struct { summary string arch string @@ -497,9 +501,15 @@ var slicerTests = []slicerTest{{ version: 22.04 components: [main, universe] default: true + v1-public-keys: [test-key] bar: version: 22.04 components: [main] + v1-public-keys: [test-key] + v1-public-keys: + test-key: + id: ` + testKey.ID + ` + armor: |` + "\n" + testutil.PrefixEachLine(testKey.PubKeyArmor, "\t\t\t\t\t\t") + ` `, "slices/mydir/base-files.yaml": ` package: base-files @@ -517,12 +527,17 @@ var slicerTests = []slicerTest{{ }, }} -const defaultChiselYaml = ` +var defaultChiselYaml = ` format: chisel-v1 archives: ubuntu: version: 22.04 components: [main, universe] + v1-public-keys: [test-key] + v1-public-keys: + test-key: + id: ` + testKey.ID + ` + armor: |` + "\n" + testutil.PrefixEachLine(testKey.PubKeyArmor, "\t\t\t\t\t\t") + ` ` type testArchive struct { diff --git a/internal/testutil/pgpkeys.go b/internal/testutil/pgpkeys.go new file mode 100644 index 00000000..e999a9a3 --- /dev/null +++ b/internal/testutil/pgpkeys.go @@ -0,0 +1,182 @@ +package testutil + +import ( + "log" + + "golang.org/x/crypto/openpgp/packet" + + "github.com/canonical/chisel/internal/pgputil" +) + +type PGPKeyData struct { + ID string + PubKeyArmor string + PrivKeyArmor string + PubKey *packet.PublicKey + PrivKey *packet.PrivateKey +} + +var PGPKeys = map[string]*PGPKeyData{ + "key-ubuntu-2018": { + ID: "871920D1991BC93C", + PubKeyArmor: pubKeyUbuntu2018Armor, + }, + "key1": { + ID: "854BAF1AA9D76600", + PubKeyArmor: pubKey1Armor, + PrivKeyArmor: privKey1Armor, + }, + "key2": { + ID: "9568570379BF1F43", + PubKeyArmor: pubKey2Armor, + PrivKeyArmor: privKey2Armor, + }, +} + +func init() { + for name, key := range PGPKeys { + if key.PubKeyArmor != "" { + pubKeys, privKeys, err := pgputil.DecodeKeys([]byte(key.PubKeyArmor)) + if err != nil || len(privKeys) > 0 || len(pubKeys) != 1 || pubKeys[0].KeyIdString() != key.ID { + log.Panicf("invalid public key armored data: %s", name) + } + key.PubKey = pubKeys[0] + } + if key.PrivKeyArmor != "" { + pubKeys, privKeys, err := pgputil.DecodeKeys([]byte(key.PrivKeyArmor)) + if err != nil || len(pubKeys) > 0 || len(privKeys) != 1 || privKeys[0].KeyIdString() != key.ID { + log.Panicf("invalid private key armored data: %s", name) + } + key.PrivKey = privKeys[0] + } + } +} + +// Ubuntu Archive Automatic Signing Key (2018) . +// ID: 871920D1991BC93C. +// Useful to validate InRelease files from live archive. +const pubKeyUbuntu2018Armor = ` +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQINBFufwdoBEADv/Gxytx/LcSXYuM0MwKojbBye81s0G1nEx+lz6VAUpIUZnbkq +dXBHC+dwrGS/CeeLuAjPRLU8AoxE/jjvZVp8xFGEWHYdklqXGZ/gJfP5d3fIUBtZ +HZEJl8B8m9pMHf/AQQdsC+YzizSG5t5Mhnotw044LXtdEEkx2t6Jz0OGrh+5Ioxq +X7pZiq6Cv19BohaUioKMdp7ES6RYfN7ol6HSLFlrMXtVfh/ijpN9j3ZhVGVeRC8k +KHQsJ5PkIbmvxBiUh7SJmfZUx0IQhNMaDHXfdZAGNtnhzzNReb1FqNLSVkrS/Pns +AQzMhG1BDm2VOSF64jebKXffFqM5LXRQTeqTLsjUbbrqR6s/GCO8UF7jfUj6I7ta +LygmsHO/JD4jpKRC0gbpUBfaiJyLvuepx3kWoqL3sN0LhlMI80+fA7GTvoOx4tpq +VlzlE6TajYu+jfW3QpOFS5ewEMdL26hzxsZg/geZvTbArcP+OsJKRmhv4kNo6Ayd +yHQ/3ZV/f3X9mT3/SPLbJaumkgp3Yzd6t5PeBu+ZQk/mN5WNNuaihNEV7llb1Zhv +Y0Fxu9BVd/BNl0rzuxp3rIinB2TX2SCg7wE5xXkwXuQ/2eTDE0v0HlGntkuZjGow +DZkxHZQSxZVOzdZCRVaX/WEFLpKa2AQpw5RJrQ4oZ/OfifXyJzP27o03wQARAQAB +tEJVYnVudHUgQXJjaGl2ZSBBdXRvbWF0aWMgU2lnbmluZyBLZXkgKDIwMTgpIDxm +dHBtYXN0ZXJAdWJ1bnR1LmNvbT6JAjgEEwEKACIFAlufwdoCGwMGCwkIBwMCBhUI +AgkKCwQWAgMBAh4BAheAAAoJEIcZINGZG8k8LHMQAKS2cnxz/5WaoCOWArf5g6UH +beOCgc5DBm0hCuFDZWWv427aGei3CPuLw0DGLCXZdyc5dqE8mvjMlOmmAKKlj1uG +g3TYCbQWjWPeMnBPZbkFgkZoXJ7/6CB7bWRht1sHzpt1LTZ+SYDwOwJ68QRp7DRa +Zl9Y6QiUbeuhq2DUcTofVbBxbhrckN4ZteLvm+/nG9m/ciopc66LwRdkxqfJ32Cy +q+1TS5VaIJDG7DWziG+Kbu6qCDM4QNlg3LH7p14CrRxAbc4lvohRgsV4eQqsIcdF +kuVY5HPPj2K8TqpY6STe8Gh0aprG1RV8ZKay3KSMpnyV1fAKn4fM9byiLzQAovC0 +LZ9MMMsrAS/45AvC3IEKSShjLFn1X1dRCiO6/7jmZEoZtAp53hkf8SMBsi78hVNr +BumZwfIdBA1v22+LY4xQK8q4XCoRcA9G+pvzU9YVW7cRnDZZGl0uwOw7z9PkQBF5 +KFKjWDz4fCk+K6+YtGpovGKekGBb8I7EA6UpvPgqA/QdI0t1IBP0N06RQcs1fUaA +QEtz6DGy5zkRhR4pGSZn+dFET7PdAjEK84y7BdY4t+U1jcSIvBj0F2B7LwRL7xGp +SpIKi/ekAXLs117bvFHaCvmUYN7JVp1GMmVFxhIdx6CFm3fxG8QjNb5tere/YqK+ +uOgcXny1UlwtCUzlrSaP +=9AdM +-----END PGP PUBLIC KEY BLOCK----- +` + +// Test-purpose RSA 2048 bits signing key-pairs without a passphrase. +// ID: 854BAF1AA9D76600. User: "foo-bar ". +const pubKey1Armor = ` +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQENBGVs8P4BCADPh/fNnw2AI1JCYf+3p4jkcFQPLVsUkoTZk8OXjCxy+UP9Jd2m +xnxat7a0JEJZa0aWCmtlSL1XR+kFKBrd7Ry5jOHYjuDKx4kTmDUbezPnjoZIGDNX +j5cdNuMLpOINZweNNWDKRdRvhj5QX89/DYwPrLkNFwwjXjlj5tjU6RUkROYJBGPe +G2ns2cZtVbYMh3FDU9YRfp/hUqGVf+UFRyUw+mo1TUlk5F7fnfwEQmsppDHvfTNJ +yjEMZD7nReTEeMy12GV2wysOwWMPEb2PSE/+Od7AKn5dFA7w3kyLCzAxYp6o7IE/ ++RY8YzAJe6GmLwhTWtylMV1xteQhZkEe/QGXABEBAAG0EWZvby1iYXIgPGZvb0Bi +YXI+iQFOBBMBCgA4FiEEDp0LAdsRnT9gfhU5hUuvGqnXZgAFAmVs8P4CGwMFCwkI +BwIGFQoJCAsCBBYCAwECHgECF4AACgkQhUuvGqnXZgCHZAf/b/rkMz2UY42LhuvJ +xDW7KbdBI+UgFp2k2tg2SkLM27GdcztpcNn/RE9U1vc8uCI05MbMhKQ+oq4RmO6i +QbCPPGy1Mgf61Fku0JTZGEKg+4DKNmnVkSpiOc03z3G2Gyi2m9G2u+HdJhXHumej +7NXkQvVFxXzDnzntbnmkM0fMfO+wdP5/EFjJbHC47yAAds/yspfk5qIHu6PHrTVB ++wJGwOJdwJ1+2zis5ONE8NexfSrDzjGJoKAFtlMwNNDZ39JlkguMB0M5SxoGRXxQ +ZE4DhPntUIW0qsE6ChmmjssjSDeg75rwgc+hjNDunKQhKNpjVVFGF4uceV5EQ084 +F4nA5w== +=ZXap +-----END PGP PUBLIC KEY BLOCK----- +` +const privKey1Armor = ` +-----BEGIN PGP PRIVATE KEY BLOCK----- + +lQOYBGVs8P4BCADPh/fNnw2AI1JCYf+3p4jkcFQPLVsUkoTZk8OXjCxy+UP9Jd2m +xnxat7a0JEJZa0aWCmtlSL1XR+kFKBrd7Ry5jOHYjuDKx4kTmDUbezPnjoZIGDNX +j5cdNuMLpOINZweNNWDKRdRvhj5QX89/DYwPrLkNFwwjXjlj5tjU6RUkROYJBGPe +G2ns2cZtVbYMh3FDU9YRfp/hUqGVf+UFRyUw+mo1TUlk5F7fnfwEQmsppDHvfTNJ +yjEMZD7nReTEeMy12GV2wysOwWMPEb2PSE/+Od7AKn5dFA7w3kyLCzAxYp6o7IE/ ++RY8YzAJe6GmLwhTWtylMV1xteQhZkEe/QGXABEBAAEAB/4jvxdbdyiTqEHchlXO +NBDbzE9mV9km53/znESl/3KOkUn5OkL+HZVA6QES8WXuUhCT+pJ6HTfj51KHXVuX +W2bFvTMPorispQcC9YY8SBHuMjoGBAkf7W9JjHE6SbnYNiVyWL3lyXZoiVaFcKNk +jphQAN/VFeG029+FyjcSIV3PY7FWI4Q1dyqyf78iWa6I400cmyGFvZDSps/oo3sT +0xcjdLL5AaXyR0FtZoSrltioYzp4cnYDI2ES9PT7uR6MQ7AwUamUQ/7dUR6zSi1o +NbHVOYItsZEsY8N/1vUxW+Ps0bbgZd9ob6n+1beQIeSMhJiW0g2NiqlZXo8GELNp +LNOBBADl+tu0iX0DCTJ5fnDeiWgMv+sPA9pcACKhnxDuOXMJjV/gGY2XtKzP0o68 +y8N5Nry0UG3wHMlgqp5qY8ZkXfH3zMmIezG5C6HZQ7A44wem3iBYj8Z1bjpT8AW7 +rFi+1iBDmZ4whHzsxLp8XB/cugAh/g3bo6rJl2bCaQPnpsSygQQA5wLnFL8pnj4M +kNzefp/ZFGTstB7AC1Dfkja9QTfimZpJZj/5XXyewAgmqQt9uersmLHfXhS3sgrk +kko74ZEZY5PCInsbcvUkgRxgw/JnjWdHLVUOMMd12RVQU9BOVf2kN8sEWCQbqzsM +H9IEtFjXXyyubmb4euI25xs1ptxk+BcD/j1J5bu6RZfP2IfEeBPu4w8zK5WOioLY +dia8kvzScIRvREB6DbYCifirx0gSuZSCyo+zm/KfZCof89ihOZ4e3OAWQDqajfQH +AGoXJCN9LRJsGe/x79LHuOx71x1MbTTvOUlYJTD9+cHzWRzKHb2ecFL6jaJb4OhY +RP4t194OXMHdQ2q0EWZvby1iYXIgPGZvb0BiYXI+iQFOBBMBCgA4FiEEDp0LAdsR +nT9gfhU5hUuvGqnXZgAFAmVs8P4CGwMFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AA +CgkQhUuvGqnXZgCHZAf/b/rkMz2UY42LhuvJxDW7KbdBI+UgFp2k2tg2SkLM27Gd +cztpcNn/RE9U1vc8uCI05MbMhKQ+oq4RmO6iQbCPPGy1Mgf61Fku0JTZGEKg+4DK +NmnVkSpiOc03z3G2Gyi2m9G2u+HdJhXHumej7NXkQvVFxXzDnzntbnmkM0fMfO+w +dP5/EFjJbHC47yAAds/yspfk5qIHu6PHrTVB+wJGwOJdwJ1+2zis5ONE8NexfSrD +zjGJoKAFtlMwNNDZ39JlkguMB0M5SxoGRXxQZE4DhPntUIW0qsE6ChmmjssjSDeg +75rwgc+hjNDunKQhKNpjVVFGF4uceV5EQ084F4nA5w== +=VBWI +-----END PGP PRIVATE KEY BLOCK----- +` + +// Test-purpose RSA 1024 bits signing key-pairs without a passphrase. +// ID: 9568570379BF1F43. User: "Extra Test Key ". +const pubKey2Armor = ` +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mI0EZXAwcgEEAMBQ4Qx6xam1k1hyjPrKQfCnGRBBm2+Lw9DHQcz0lreH51iZEVkS +fACbPHI9A7NX8xdX1cMLpaTQCT3h30WwuLuNAo1IdYcdGpfzFzd6rqS5OCItj+3u +XZrTlS8QxVVShSPYFfxYaIXKCZF9G+RTKD0rWQwkMwNHZ4vJGBm7qKytABEBAAG0 +GUV4dHJhIFRlc3QgS2V5IDx0ZXN0QGtleT6IzgQTAQoAOBYhBFQrMoAoDwtUfB+G +BpVoVwN5vx9DBQJlcDByAhsDBQsJCAcCBhUKCQgLAgQWAgMBAh4BAheAAAoJEJVo +VwN5vx9Dy80D/iUzJkfT8lsH0vZ2jcpgcyjtZqrIfOMLYk8DqoYD/1wDGx4TIzg/ +bpqDHxBCDmBaxY6+ps9IaBcsD1whjyX4AZK6FykV8d9GAc+3b9t2EPe92LV3XKaT +rwF9bjDSJZUUz1I31YTnHpBiRU+hWuf7OVjnLcEAB8mMa7Y6YN37qT44 +=U79/ +-----END PGP PUBLIC KEY BLOCK----- +` +const privKey2Armor = ` +-----BEGIN PGP PRIVATE KEY BLOCK----- + +lQHYBGVwMHIBBADAUOEMesWptZNYcoz6ykHwpxkQQZtvi8PQx0HM9Ja3h+dYmRFZ +EnwAmzxyPQOzV/MXV9XDC6Wk0Ak94d9FsLi7jQKNSHWHHRqX8xc3eq6kuTgiLY/t +7l2a05UvEMVVUoUj2BX8WGiFygmRfRvkUyg9K1kMJDMDR2eLyRgZu6isrQARAQAB +AAP+LXyDuiSor0rt0o/ndeLURVP0auKlnbS4SB902gHoyvh3OL6deoyTbT5KRffV +8fuFmNoSymrtDwYQhYUwvqY9jt+lVSKDseqLkF5C92VZFWpjiYDOqZzoBfVUDZo5 +NffyIxuG5X33o9yBmUk29PWcLqzSanxg/TmXy63pp4sBYfECAN3GgiWxwrQTtv0X +OUuSKbvnDVyM86R7Hdo08hmwB/6qhGibw5KBko+h+kBsIo1naEzzGsXWUjLk8BbZ +qPTRGrECAN3+ijctJPm+JprWjJlJ5KrdXlIMG5x87vtdp5ZzctsmY97GMBaW+SvW +uuBHfiY7xFUru8304gWd/YAwTdxVeL0CALjGKCTWPhZaRJ+ew9iryVgFEznaNAgO +pzVXr3yllNdinGWjvbyEkn1y7OlzH0REg9jOsc82Bbz4aiDm19Qr/1KtR7QZRXh0 +cmEgVGVzdCBLZXkgPHRlc3RAa2V5PojOBBMBCgA4FiEEVCsygCgPC1R8H4YGlWhX +A3m/H0MFAmVwMHICGwMFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AACgkQlWhXA3m/ +H0PLzQP+JTMmR9PyWwfS9naNymBzKO1mqsh84wtiTwOqhgP/XAMbHhMjOD9umoMf +EEIOYFrFjr6mz0hoFywPXCGPJfgBkroXKRXx30YBz7dv23YQ973YtXdcppOvAX1u +MNIllRTPUjfVhOcekGJFT6Fa5/s5WOctwQAHyYxrtjpg3fupPjg= +=JbF+ +-----END PGP PRIVATE KEY BLOCK----- +` diff --git a/internal/testutil/reindent.go b/internal/testutil/reindent.go index 10c68a0d..d4ff16ff 100644 --- a/internal/testutil/reindent.go +++ b/internal/testutil/reindent.go @@ -38,3 +38,26 @@ func Reindent(in string) []byte { } return buf.Bytes() } + +// PrefixEachLine indents each line in the provided string with the prefix. +func PrefixEachLine(text string, prefix string) string { + var result strings.Builder + lines := strings.Split(text, "\n") + lastNewline := false + if strings.HasSuffix(text, "\n") { + // Skip iterating over the empty line. + lines = lines[:len(lines)-1] + lastNewline = true + } + for i, line := range lines { + result.WriteString(prefix) + result.WriteString(line) + if i == len(lines)-1 && !lastNewline { + // Do not add the last newline if the text did not have it to begin with. + continue + } else { + result.WriteString("\n") + } + } + return result.String() +} diff --git a/internal/testutil/reindent_test.go b/internal/testutil/reindent_test.go index d3b74a59..4b14cc9c 100644 --- a/internal/testutil/reindent_test.go +++ b/internal/testutil/reindent_test.go @@ -3,8 +3,9 @@ package testutil_test import ( "strings" - "github.com/canonical/chisel/internal/testutil" . "gopkg.in/check.v1" + + "github.com/canonical/chisel/internal/testutil" ) type reindentTest struct { @@ -70,3 +71,38 @@ func (*S) testReindent(c *C, test reindentTest) { } c.Assert(string(reindented), Equals, test.result) } + +type prefixEachLineTest struct { + raw, prefix, result string +} + +var prefixEachLineTests = []prefixEachLineTest{{ + raw: "a\n\tb\n \t\tc\td\n\t ", + prefix: "foo", + result: "fooa\nfoo\tb\nfoo \t\tc\td\nfoo\t ", +}, { + raw: "foo", + prefix: "pref", + result: "preffoo", +}, { + raw: "", + prefix: "p", + result: "p", +}, { + raw: "\n", + prefix: "\t", + result: "\t\n", +}, { + raw: "\n\n", + prefix: "\t", + result: "\t\n\t\n", +}} + +func (s *S) TestPrefixEachLine(c *C) { + for _, test := range prefixEachLineTests { + c.Logf("Test: %#v", test) + + prefixed := testutil.PrefixEachLine(test.raw, test.prefix) + c.Assert(prefixed, Equals, test.result) + } +}