diff --git a/docs/imagecustomizer/api/cli/cli.md b/docs/imagecustomizer/api/cli/cli.md index 904fd26049..bbcc665402 100644 --- a/docs/imagecustomizer/api/cli/cli.md +++ b/docs/imagecustomizer/api/cli/cli.md @@ -21,12 +21,13 @@ Added in v0.3. ## --image-file=FILE-PATH -Required, unless [input.image.path](../configuration/inputImage.md#path-string) is -provided in the configuration file. If both `input.image.path` and -`--image-file` are provided, then the `--image-file` value is used. - The base image file to customize. +An input image must either be provided in the configuration file (e.g. +[input.image.path](../configuration/inputImage.md#path-string)) or on the command line. +If both a command-line input image and a configuration input image are specified, then +the command line's input image overrides the config file's input image. + This file is typically one of the standard Azure Linux core images. But it can also be an Azure Linux image that has been customized. diff --git a/docs/imagecustomizer/api/configuration/azurelinuximage.md b/docs/imagecustomizer/api/configuration/azurelinuximage.md new file mode 100644 index 0000000000..1530e0dea4 --- /dev/null +++ b/docs/imagecustomizer/api/configuration/azurelinuximage.md @@ -0,0 +1,70 @@ +--- +parent: Configuration +ancestor: Image Customizer +--- + +# azureLinuxImage type + +Specifies an Azure Linux image to be used as the base image. + +This feature is in preview and may be subject to breaking changes. +You may enable this feature by adding `input-image-oci` to the +[previewfeatures](../configuration/config.md#previewfeatures-string) API. + +Example: + +```yaml +previewFeatures: +- input-image-oci + +input: + image: + azureLinux: + variant: minimal-os + version: 3.0 +``` + +The Azure Linux images are downloaded from the +[Microsoft Artifact Registry](https://mcr.microsoft.com) (MCR). + +The URI used is: + +| Version | URI | +| ------------ | --------------------------------------------------------------- | +| 2.0 | mcr.microsoft.com/azurelinux/2.0/image/``:latest | +| 3.0 | mcr.microsoft.com/azurelinux/3.0/image/``:latest | +| 2.0.`` | mcr.microsoft.com/azurelinux/2.0/image/``:2.0.`` | +| 3.0.`` | mcr.microsoft.com/azurelinux/3.0/image/``:3.0.`` | + +The list of Azure Linux image versions is available on MCR. +For example: the +[Azure Linux 3.0 minimal-os versions](https://mcr.microsoft.com/en-us/artifact/mar/azurelinux/3.0/image/minimal-os/tags). + +Added in v1.1. + +## version [string] + +Specifies the version of the Azure Linux image to use as the base image. + +This value is required. + +Supported values include: + +- `2.0` +- `3.0` +- `2.0.` (e.g. `2.0.20251010`) +- `3.0.` (e.g. `3.0.20250910`) + +Added in v1.1. + +## variant [[ociPlatform](ociplatform.md)] + +Specifies the variant of the Azure Linux image to use as the base image. + +This value is required. + +Example values: + +- `minimal-os` + +Added in v1.1. diff --git a/docs/imagecustomizer/api/configuration/inputImage.md b/docs/imagecustomizer/api/configuration/inputImage.md index ea71da60c7..caa62dcf35 100644 --- a/docs/imagecustomizer/api/configuration/inputImage.md +++ b/docs/imagecustomizer/api/configuration/inputImage.md @@ -7,19 +7,23 @@ ancestor: Image Customizer Specifies the configuration for the input image. +Only one child field (`path`, `oci`, or `azureLinux`) may be specified. + +An input image must either be provided in the configuration file or on the command line +(e.g. [--image-file](../cli/cli.md#--image-filefile-path)). +If both a command-line input image and a configuration input image are specified, then +the command line's input image overrides the config file's input image. + Example: ```yaml -image: - path: ./base.vhdx +input: + image: + path: ./base.vhdx ``` ## path [string] -Required, unless [--image-file](../cli/cli.md#--image-filefile-path) is -provided on the command line. If both `--image-file` and -`input.image.path` are provided, then the value of `--image-file` is used. - The base image file to customize. This file is typically one of the standard Azure Linux core images. @@ -60,3 +64,13 @@ You may enable this feature by adding `input-image-oci` to the [previewfeatures](../configuration/config.md#previewfeatures-string) API. Added in v1.1. + +## azureLinux [[azureLinuxImage](azurelinuximage.md)] + +Download an Azure Linux image file to use as the base image. + +This feature is in preview and may be subject to breaking changes. +You may enable this feature by adding `input-image-oci` to the +[previewfeatures](../configuration/config.md#previewfeatures-string) API. + +Added in v1.1. diff --git a/docs/imagecustomizer/api/configuration/ociimage.md b/docs/imagecustomizer/api/configuration/ociimage.md index bb6f6f3d5e..5ad8b341a9 100644 --- a/docs/imagecustomizer/api/configuration/ociimage.md +++ b/docs/imagecustomizer/api/configuration/ociimage.md @@ -35,6 +35,10 @@ This feature is in preview and may be subject to breaking changes. You may enable this feature by adding `input-image-oci` to the [previewfeatures](../configuration/config.md#previewfeatures-string) API. +When using official Azure Linux images from the Microsoft Artifact Registry (MCR), it is +recommended that you use the dedicated +[input.image.azureLinux](./inputImage.md#azurelinux-azurelinuximage) API. + Added in v1.1. ## uri [string] diff --git a/go.mod b/go.mod index 879a258434..0fc3fb0635 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( github.com/klauspost/compress v1.18.0 github.com/klauspost/pgzip v1.2.6 github.com/moby/sys/mountinfo v0.7.2 + github.com/notaryproject/notation-go v1.3.2 github.com/opencontainers/image-spec v1.1.1 github.com/sirupsen/logrus v1.9.3 github.com/stretchr/testify v1.11.1 @@ -27,6 +28,7 @@ require ( ) require ( + github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b // indirect github.com/bahlo/generic-list-go v0.2.0 // indirect @@ -35,14 +37,21 @@ require ( github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/fxamacker/cbor/v2 v2.8.0 // indirect + github.com/go-asn1-ber/asn1-ber v1.5.7 // indirect + github.com/go-ldap/ldap/v3 v3.4.10 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect + github.com/golang-jwt/jwt/v4 v4.5.2 // indirect github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/notaryproject/notation-core-go v1.3.0 // indirect + github.com/notaryproject/notation-plugin-framework-go v1.0.0 // indirect + github.com/notaryproject/tspclient-go v1.0.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_golang v1.23.0 // indirect @@ -50,7 +59,9 @@ require ( github.com/prometheus/common v0.65.0 // indirect github.com/prometheus/otlptranslator v0.0.2 // indirect github.com/prometheus/procfs v0.17.0 // indirect + github.com/veraison/go-cose v1.3.0 // indirect github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect + github.com/x448/float16 v0.8.4 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/contrib/bridges/prometheus v0.63.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.14.0 // indirect @@ -70,6 +81,8 @@ require ( go.opentelemetry.io/otel/sdk/metric v1.38.0 // indirect go.opentelemetry.io/otel/trace v1.38.0 // indirect go.opentelemetry.io/proto/otlp v1.7.1 // indirect + golang.org/x/crypto v0.41.0 // indirect + golang.org/x/mod v0.26.0 // indirect golang.org/x/net v0.43.0 // indirect golang.org/x/sync v0.16.0 // indirect golang.org/x/text v0.28.0 // indirect diff --git a/go.sum b/go.sum index f7adabfc97..ebe17d6d71 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8= +github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= github.com/alecthomas/kong v1.12.1 h1:iq6aMJDcFYP9uFrLdsiZQ2ZMmcshduyGv4Pek0MQPW0= @@ -8,6 +10,8 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafo github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b h1:mimo19zliBX/vSQ6PWWSL9lK8qwHozUj03+zLoEB8O0= github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b/go.mod h1:fvzegU4vN3H1qMT+8wDmzjAcDONcgo2/SZ/TyfdUOFs= +github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI= +github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= @@ -27,25 +31,51 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/fxamacker/cbor/v2 v2.8.0 h1:fFtUGXUzXPHTIUdne5+zzMPTfffl3RD5qYnkY40vtxU= +github.com/fxamacker/cbor/v2 v2.8.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +github.com/go-asn1-ber/asn1-ber v1.5.7 h1:DTX+lbVTWaTw1hQ+PbZPlnDZPEIs0SS/GCZAl535dDk= +github.com/go-asn1-ber/asn1-ber v1.5.7/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= +github.com/go-ldap/ldap/v3 v3.4.10 h1:ot/iwPOhfpNVgB1o+AVXljizWZ9JTp7YF5oeyONmcJU= +github.com/go-ldap/ldap/v3 v3.4.10/go.mod h1:JXh4Uxgi40P6E9rdsYqpUtbW46D9UTjJ9QSwGRznplY= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= +github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= +github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc h1:GN2Lv3MGO7AS6PrRoT6yV5+wkrOpcszoIsO4+4ds248= github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc/go.mod h1:+JKpmjMGhpgPL+rXZ5nsZieVzvarn86asRlBg4uNGnk= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs= +github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= +github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E= github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0= +github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8= +github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= +github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo= +github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= +github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg= +github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo= +github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o= +github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= +github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8= +github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= +github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY= +github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= @@ -68,6 +98,14 @@ github.com/moby/sys/mountinfo v0.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9Kou github.com/moby/sys/mountinfo v0.7.2/go.mod h1:1YOa8w8Ih7uW0wALDUgT1dTTSBrZ+HiBLGws92L2RU4= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/notaryproject/notation-core-go v1.3.0 h1:mWJaw1QBpBxpjLSiKOjzbZvB+xh2Abzk14FHWQ+9Kfs= +github.com/notaryproject/notation-core-go v1.3.0/go.mod h1:hzvEOit5lXfNATGNBT8UQRx2J6Fiw/dq/78TQL8aE64= +github.com/notaryproject/notation-go v1.3.2 h1:4223iLXOHhEV7ZPzIUJEwwMkhlgzoYFCsMJvSH1Chb8= +github.com/notaryproject/notation-go v1.3.2/go.mod h1:/1kuq5WuLF6Gaer5re0Z6HlkQRlKYO4EbWWT/L7J1Uw= +github.com/notaryproject/notation-plugin-framework-go v1.0.0 h1:6Qzr7DGXoCgXEQN+1gTZWuJAZvxh3p8Lryjn5FaLzi4= +github.com/notaryproject/notation-plugin-framework-go v1.0.0/go.mod h1:RqWSrTOtEASCrGOEffq0n8pSg2KOgKYiWqFWczRSics= +github.com/notaryproject/tspclient-go v1.0.0 h1:AwQ4x0gX8IHnyiZB1tggpn5NFqHpTEm1SDX8YNv4Dg4= +github.com/notaryproject/tspclient-go v1.0.0/go.mod h1:LGyA/6Kwd2FlM0uk8Vc5il3j0CddbWSHBj/4kxQDbjs= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= @@ -92,15 +130,22 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/veraison/go-cose v1.3.0 h1:2/H5w8kdSpQJyVtIhx8gmwPJ2uSz1PkyWFx0idbd7rk= +github.com/veraison/go-cose v1.3.0/go.mod h1:df09OV91aHoQWLmy1KsDdYiagtXgyAwAl8vFeFn1gMc= github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/contrib/bridges/prometheus v0.63.0 h1:/Rij/t18Y7rUayNg7Id6rPrEnHgorxYabm2E6wUdPP4= @@ -149,17 +194,87 @@ go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOV go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +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.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= +golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= +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/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg= +golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/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.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= +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/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/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-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/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.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= +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.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= +golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= +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.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +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/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 h1:BIRfGDEjiHRrk0QKZe3Xv2ieMhtgRGeLcZQ0mIVn4EY= @@ -177,6 +292,7 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/toolkit/tools/imagecustomizer/container/notation/notation-setup.sh b/toolkit/tools/imagecustomizer/container/notation/notation-setup.sh index 8472eb6c10..5da06a3083 100755 --- a/toolkit/tools/imagecustomizer/container/notation/notation-setup.sh +++ b/toolkit/tools/imagecustomizer/container/notation/notation-setup.sh @@ -1,6 +1,7 @@ set -eux SCRIPT_DIR="$(dirname "${BASH_SOURCE[0]}")" +CERT_DIR="$SCRIPT_DIR/../../../internal/resources/certificates" -notation cert add --type ca --store microsoft-supplychain-2022 "$SCRIPT_DIR/Microsoft Supply Chain RSA Root CA 2022.crt" +notation cert add --type ca --store microsoft-supplychain-2022 "$CERT_DIR/Microsoft Supply Chain RSA Root CA 2022.crt" notation policy import --force "$SCRIPT_DIR/trustpolicy.json" diff --git a/toolkit/tools/imagecustomizerapi/azurelinuximage.go b/toolkit/tools/imagecustomizerapi/azurelinuximage.go new file mode 100644 index 0000000000..ed861b54a5 --- /dev/null +++ b/toolkit/tools/imagecustomizerapi/azurelinuximage.go @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +package imagecustomizerapi + +import ( + "fmt" + "regexp" +) + +var ( + azureLinuxVersionRegex = regexp.MustCompile(`^(\d+\.\d+)(\.(\d+))?$`) + + // Limit Azure Linux variant name to what is permitted in a subsegment of an OCI repository path. + // See, OCI Spec v1.1.1: https://github.com/opencontainers/distribution-spec/blob/v1.1.1/spec.md#pulling-manifests + variantRegexp = regexp.MustCompile(`^[a-z0-9]+((\.|_|__|-+)[a-z0-9]+)*$`) +) + +type AzureLinuxImage struct { + Version string `yaml:"version" json:"version,omitempty"` + Variant string `yaml:"variant" json:"variant,omitempty"` +} + +func (i *AzureLinuxImage) IsValid() error { + variantValid := variantRegexp.MatchString(i.Variant) + if !variantValid { + return fmt.Errorf("invalid 'variant' field (value='%s')", i.Variant) + } + + _, _, err := i.ParseVersion() + if err != nil { + return fmt.Errorf("invalid 'version' field:\n%w", err) + } + + return nil +} + +func (i *AzureLinuxImage) ParseVersion() (string, string, error) { + groups := azureLinuxVersionRegex.FindStringSubmatch(i.Version) + if groups == nil { + return "", "", fmt.Errorf("invalid version value, expecting .. (value='%s')", i.Version) + } + + majorMinor := groups[1] + date := groups[3] + + return majorMinor, date, nil +} diff --git a/toolkit/tools/imagecustomizerapi/azurelinuximage_test.go b/toolkit/tools/imagecustomizerapi/azurelinuximage_test.go new file mode 100644 index 0000000000..372669e424 --- /dev/null +++ b/toolkit/tools/imagecustomizerapi/azurelinuximage_test.go @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +package imagecustomizerapi + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestAzureLinuxImageIsValidOk(t *testing.T) { + value := AzureLinuxImage{ + Variant: "minimal-os", + Version: "3.0.20250910", + } + assert.NoError(t, value.IsValid()) +} + +func TestInputImageIsValidOciBadVariant(t *testing.T) { + value := AzureLinuxImage{ + Variant: "_minimal-os", + Version: "3.0.20250910", + } + err := value.IsValid() + assert.ErrorContains(t, err, "invalid 'variant' field") +} + +func TestInputImageIsValidOciBadVersion(t *testing.T) { + value := AzureLinuxImage{ + Variant: "minimal-os", + Version: "3", + } + err := value.IsValid() + assert.ErrorContains(t, err, "invalid 'version' field") +} diff --git a/toolkit/tools/imagecustomizerapi/inputImage.go b/toolkit/tools/imagecustomizerapi/inputImage.go index e352d7806b..40490ac9ab 100644 --- a/toolkit/tools/imagecustomizerapi/inputImage.go +++ b/toolkit/tools/imagecustomizerapi/inputImage.go @@ -8,13 +8,38 @@ import ( ) type InputImage struct { - Path string `yaml:"path" json:"path,omitempty"` - Oci *OciImage `yaml:"oci" json:"oci,omitempty"` + Path string `yaml:"path" json:"path,omitempty"` + Oci *OciImage `yaml:"oci" json:"oci,omitempty"` + AzureLinux *AzureLinuxImage `yaml:"azureLinux" json:"azureLinux,omitempty"` } func (ii *InputImage) IsValid() error { - if ii.Path != "" && ii.Oci != nil { - return fmt.Errorf("cannot specify both 'path' and 'oci'") + count := 0 + if ii.Path != "" { + count++ + } + if ii.Oci != nil { + count++ + } + if ii.AzureLinux != nil { + count++ + } + if count > 1 { + return fmt.Errorf("must only specify one of 'path', 'oci', and 'azureLinux'") + } + + if ii.Oci != nil { + err := ii.Oci.IsValid() + if err != nil { + return fmt.Errorf("invalid 'oci' field:\n%w", err) + } + } + + if ii.AzureLinux != nil { + err := ii.AzureLinux.IsValid() + if err != nil { + return fmt.Errorf("invalid 'azureLinux' field:\n%w", err) + } } return nil diff --git a/toolkit/tools/imagecustomizerapi/inputImage_test.go b/toolkit/tools/imagecustomizerapi/inputImage_test.go index 8c5b33b6dd..250d5a4987 100644 --- a/toolkit/tools/imagecustomizerapi/inputImage_test.go +++ b/toolkit/tools/imagecustomizerapi/inputImage_test.go @@ -19,7 +19,7 @@ func TestInputImageIsValidPath(t *testing.T) { assert.NoError(t, ii.IsValid()) } -func TestInputImageIsValidOci(t *testing.T) { +func TestInputImageIsValidOciOk(t *testing.T) { ii := InputImage{ Oci: &OciImage{ Uri: "mcr.microsoft.com/azurelinux/3.0/image/minimal-os:latest", @@ -28,6 +28,39 @@ func TestInputImageIsValidOci(t *testing.T) { assert.NoError(t, ii.IsValid()) } +func TestInputImageIsValidOciBad(t *testing.T) { + ii := InputImage{ + Oci: &OciImage{ + Uri: "mcr.microsoft.com", + }, + } + err := ii.IsValid() + assert.ErrorContains(t, err, "invalid 'oci' field") + assert.ErrorContains(t, err, "invalid 'uri' field") +} + +func TestInputImageIsValidAZLOk(t *testing.T) { + ii := InputImage{ + AzureLinux: &AzureLinuxImage{ + Variant: "minimal-os", + Version: "3.0", + }, + } + assert.NoError(t, ii.IsValid()) +} + +func TestInputImageIsValidAZLBad(t *testing.T) { + ii := InputImage{ + AzureLinux: &AzureLinuxImage{ + Variant: "minimal-os", + Version: "3.0.0.0", + }, + } + err := ii.IsValid() + assert.ErrorContains(t, err, "invalid 'azureLinux' field") + assert.ErrorContains(t, err, "invalid 'version' field") +} + func TestInputImageIsValidBothOciAndPath(t *testing.T) { ii := InputImage{ Path: "image.vhdx", @@ -36,5 +69,17 @@ func TestInputImageIsValidBothOciAndPath(t *testing.T) { }, } err := ii.IsValid() - assert.ErrorContains(t, err, "cannot specify both 'path' and 'oci'") + assert.ErrorContains(t, err, "must only specify one of 'path', 'oci', and 'azureLinux'") +} + +func TestInputImageIsValidBothAZLAndPath(t *testing.T) { + ii := InputImage{ + Path: "image.vhdx", + AzureLinux: &AzureLinuxImage{ + Variant: "minimal-os", + Version: "3.0", + }, + } + err := ii.IsValid() + assert.ErrorContains(t, err, "must only specify one of 'path', 'oci', and 'azureLinux'") } diff --git a/toolkit/tools/imagecustomizerapi/schema.json b/toolkit/tools/imagecustomizerapi/schema.json index 1134b623ce..b194870d95 100644 --- a/toolkit/tools/imagecustomizerapi/schema.json +++ b/toolkit/tools/imagecustomizerapi/schema.json @@ -45,6 +45,18 @@ "additionalProperties": false, "type": "object" }, + "AzureLinuxImage": { + "properties": { + "version": { + "type": "string" + }, + "variant": { + "type": "string" + } + }, + "additionalProperties": false, + "type": "object" + }, "BaseConfig": { "properties": { "path": { @@ -225,6 +237,9 @@ }, "oci": { "$ref": "#/$defs/OciImage" + }, + "azureLinux": { + "$ref": "#/$defs/AzureLinuxImage" } }, "additionalProperties": false, diff --git a/toolkit/tools/imagecustomizer/container/notation/Microsoft Supply Chain RSA Root CA 2022.crt b/toolkit/tools/internal/resources/certificates/Microsoft Supply Chain RSA Root CA 2022.crt similarity index 100% rename from toolkit/tools/imagecustomizer/container/notation/Microsoft Supply Chain RSA Root CA 2022.crt rename to toolkit/tools/internal/resources/certificates/Microsoft Supply Chain RSA Root CA 2022.crt diff --git a/toolkit/tools/internal/resources/resources.go b/toolkit/tools/internal/resources/resources.go index b66f8afb96..963fe5fd58 100644 --- a/toolkit/tools/internal/resources/resources.go +++ b/toolkit/tools/internal/resources/resources.go @@ -8,6 +8,7 @@ import ( ) const ( + // Assets AssetsGrubCfgFile = "assets/grub2/grub.cfg" AssetsGrubDefFile = "assets/grub2/grub" @@ -16,7 +17,10 @@ const ( VerityMountBootPartitionGeneratorFile = "verity-signature/90mountbootpartition/mountbootpartition-generator.sh" VerityMountBootPartitionGenRulesFile = "verity-signature/90mountbootpartition/mountbootpartition-genrules.sh" VerityMountBootPartitionScriptFile = "verity-signature/90mountbootpartition/mountbootpartition.sh" + + // Certificates + MicrosoftSupplyChainRSARootCA2022File = "certificates/Microsoft Supply Chain RSA Root CA 2022.crt" ) -//go:embed assets verity-signature +//go:embed assets verity-signature certificates var ResourcesFS embed.FS diff --git a/toolkit/tools/pkg/imagecustomizerlib/baseconfigs_test.go b/toolkit/tools/pkg/imagecustomizerlib/baseconfigs_test.go index 0813323156..c7621d72c8 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/baseconfigs_test.go +++ b/toolkit/tools/pkg/imagecustomizerlib/baseconfigs_test.go @@ -33,7 +33,7 @@ func TestBaseConfigsInputAndOutput(t *testing.T) { expectedOutputPath := file.GetAbsPathWithBase(testDir, "./out/output-image-2.vhdx") expectedArtifactsPath := file.GetAbsPathWithBase(testDir, "./artifacts-2") - assert.Equal(t, expectedInputPath, rc.InputImageFile) + assert.Equal(t, expectedInputPath, rc.InputImage.Path) assert.Equal(t, expectedOutputPath, rc.OutputImageFile) assert.Equal(t, expectedArtifactsPath, rc.OutputArtifacts.Path) assert.Equal(t, "testname", rc.Config.OS.Hostname) diff --git a/toolkit/tools/pkg/imagecustomizerlib/configvalidation.go b/toolkit/tools/pkg/imagecustomizerlib/configvalidation.go index eee4428fc0..ae35aa062f 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/configvalidation.go +++ b/toolkit/tools/pkg/imagecustomizerlib/configvalidation.go @@ -95,7 +95,7 @@ func ValidateConfig(ctx context.Context, baseConfigPath string, config *imagecus } if !newImage { - rc.InputImageFile, rc.InputImageOci, err = validateInput(rc.ConfigChain, options.InputImageFile) + rc.InputImage, err = validateInput(rc.ConfigChain, options.InputImageFile) if err != nil { return nil, err } @@ -146,14 +146,19 @@ func ValidateConfigPostImageDownload(rc *ResolvedConfig) error { } func validateInput(configChain []*ConfigWithBasePath, inputImageFile string, -) (string, *imagecustomizerapi.OciImage, error) { +) (imagecustomizerapi.InputImage, error) { if inputImageFile != "" { if yes, err := file.IsFile(inputImageFile); err != nil { - return "", nil, fmt.Errorf("%w (file='%s'):\n%w", ErrInvalidInputImageFileArg, inputImageFile, err) + err = fmt.Errorf("%w (file='%s'):\n%w", ErrInvalidInputImageFileArg, inputImageFile, err) + return imagecustomizerapi.InputImage{}, err } else if !yes { - return "", nil, fmt.Errorf("%w (file='%s')", ErrInputImageFileNotFile, inputImageFile) + err = fmt.Errorf("%w (file='%s')", ErrInputImageFileNotFile, inputImageFile) + return imagecustomizerapi.InputImage{}, err } - return inputImageFile, nil, nil + + return imagecustomizerapi.InputImage{ + Path: inputImageFile, + }, nil } // Resolve input image path @@ -166,20 +171,32 @@ func validateInput(configChain []*ConfigWithBasePath, inputImageFile string, // Validate the path if yes, err := file.IsFile(inputImageAbsPath); err != nil { - return "", nil, fmt.Errorf("%w (path='%s'):\n%w", ErrInvalidInputImageFileConfig, configWithBase.Config.Input.Image.Path, err) + err = fmt.Errorf("%w (path='%s'):\n%w", ErrInvalidInputImageFileConfig, configWithBase.Config.Input.Image.Path, err) + return imagecustomizerapi.InputImage{}, err } else if !yes { - return "", nil, fmt.Errorf("%w (path='%s')", ErrInputImageFileNotFile, configWithBase.Config.Input.Image.Path) + err = fmt.Errorf("%w (path='%s')", ErrInputImageFileNotFile, configWithBase.Config.Input.Image.Path) + return imagecustomizerapi.InputImage{}, err } - return inputImageAbsPath, nil, nil + return imagecustomizerapi.InputImage{ + Path: inputImageAbsPath, + }, nil } if configWithBase.Config.Input.Image.Oci != nil { - return "", configWithBase.Config.Input.Image.Oci, nil + return imagecustomizerapi.InputImage{ + Oci: configWithBase.Config.Input.Image.Oci, + }, nil + } + + if configWithBase.Config.Input.Image.AzureLinux != nil { + return imagecustomizerapi.InputImage{ + AzureLinux: configWithBase.Config.Input.Image.AzureLinux, + }, nil } } - return "", nil, ErrInputImageFileRequired + return imagecustomizerapi.InputImage{}, ErrInputImageFileRequired } func validateAdditionalFiles(baseConfigPath string, additionalFiles imagecustomizerapi.AdditionalFileList) error { diff --git a/toolkit/tools/pkg/imagecustomizerlib/downloadimage.go b/toolkit/tools/pkg/imagecustomizerlib/downloadimage.go new file mode 100644 index 0000000000..9e46858854 --- /dev/null +++ b/toolkit/tools/pkg/imagecustomizerlib/downloadimage.go @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +package imagecustomizerlib + +import ( + "context" + "fmt" + + "github.com/microsoft/azure-linux-image-tools/toolkit/tools/imagecustomizerapi" + "github.com/microsoft/azure-linux-image-tools/toolkit/tools/internal/logger" +) + +var ( + ErrDownloadImageOci = NewImageCustomizerError("DownloadImage:Oci", "failed to download image from OCI artifact") + ErrDownloadImageAzureLinux = NewImageCustomizerError("DownloadImage:AzureLinux", "failed to download Azure Linux image") +) + +func downloadImage(ctx context.Context, inputImage imagecustomizerapi.InputImage, buildDir string, imageCacheDir string, +) (string, error) { + switch { + case inputImage.Path != "": + return inputImage.Path, nil + + case inputImage.Oci != nil: + logger.Log.Infof("Downloading OCI image (%s)", inputImage.Oci.Uri) + + inputImageFilePath, err := downloadOciImage(ctx, *inputImage.Oci, buildDir, imageCacheDir, nil) + if err != nil { + return "", fmt.Errorf("%w:\n%w", ErrDownloadImageOci, err) + } + + return inputImageFilePath, nil + + case inputImage.AzureLinux != nil: + logger.Log.Infof("Downloading Azure Linux image (%s:%s)", inputImage.AzureLinux.Variant, + inputImage.AzureLinux.Version) + + inputImageFilePath, err := downloadAzureLinuxImage(ctx, *inputImage.AzureLinux, buildDir, imageCacheDir) + if err != nil { + return "", fmt.Errorf("%w:\n%w", ErrDownloadImageAzureLinux, err) + } + + return inputImageFilePath, nil + + default: + panic("inputImage doesn't contain a value") + } +} diff --git a/toolkit/tools/pkg/imagecustomizerlib/downloadimageazurelinux.go b/toolkit/tools/pkg/imagecustomizerlib/downloadimageazurelinux.go new file mode 100644 index 0000000000..e7449b5fd8 --- /dev/null +++ b/toolkit/tools/pkg/imagecustomizerlib/downloadimageazurelinux.go @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +package imagecustomizerlib + +import ( + "context" + "fmt" + + "github.com/microsoft/azure-linux-image-tools/toolkit/tools/imagecustomizerapi" + "github.com/microsoft/azure-linux-image-tools/toolkit/tools/internal/resources" +) + +func downloadAzureLinuxImage(ctx context.Context, inputImage imagecustomizerapi.AzureLinuxImage, buildDir string, + imageCacheDir string, +) (string, error) { + ociImage, err := generateAzureLinuxOciUri(inputImage) + if err != nil { + return "", err + } + + signatureOptions := &ociSignatureCheckOptions{ + TrustPolicyName: "mcr-azure-linux", + TrustStoreName: "microsoft-supplychain", + CertificateFs: resources.ResourcesFS, + CertificateFsPath: resources.MicrosoftSupplyChainRSARootCA2022File, + } + + inputImageFilePath, err := downloadOciImage(ctx, ociImage, buildDir, imageCacheDir, signatureOptions) + if err != nil { + return "", err + } + + return inputImageFilePath, nil +} + +func generateAzureLinuxOciUri(inputImage imagecustomizerapi.AzureLinuxImage) (imagecustomizerapi.OciImage, error) { + majorMinor, date, err := inputImage.ParseVersion() + if err != nil { + return imagecustomizerapi.OciImage{}, err + } + + tag := "latest" + if date != "" { + tag = majorMinor + "." + date + } + + // Note: 'majorMinor', 'tag' and 'variant' are already sanitized. + // So, there is no need to escape the values. + uri := fmt.Sprintf("mcr.microsoft.com/azurelinux/%s/image/%s:%s", majorMinor, inputImage.Variant, tag) + ociImage := imagecustomizerapi.OciImage{ + Uri: uri, + } + + return ociImage, nil +} diff --git a/toolkit/tools/pkg/imagecustomizerlib/downloadimageazurelinux_test.go b/toolkit/tools/pkg/imagecustomizerlib/downloadimageazurelinux_test.go new file mode 100644 index 0000000000..f0940c0b3d --- /dev/null +++ b/toolkit/tools/pkg/imagecustomizerlib/downloadimageazurelinux_test.go @@ -0,0 +1,87 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +package imagecustomizerlib + +import ( + "os" + "path/filepath" + "testing" + + "github.com/microsoft/azure-linux-image-tools/toolkit/tools/imagecustomizerapi" + "github.com/microsoft/azure-linux-image-tools/toolkit/tools/internal/testutils" + "github.com/stretchr/testify/assert" +) + +func TestGenerateAzureLinuxOciUriAZL2Latest(t *testing.T) { + ociImage, err := generateAzureLinuxOciUri(imagecustomizerapi.AzureLinuxImage{ + Variant: "minimal-os", + Version: "2.0", + }) + assert.NoError(t, err) + assert.Equal(t, "mcr.microsoft.com/azurelinux/2.0/image/minimal-os:latest", ociImage.Uri) +} + +func TestGenerateAzureLinuxOciUriAZL3Latest(t *testing.T) { + ociImage, err := generateAzureLinuxOciUri(imagecustomizerapi.AzureLinuxImage{ + Variant: "minimal-os", + Version: "3.0", + }) + assert.NoError(t, err) + assert.Equal(t, "mcr.microsoft.com/azurelinux/3.0/image/minimal-os:latest", ociImage.Uri) +} + +func TestGenerateAzureLinuxOciUriAZL2Date(t *testing.T) { + ociImage, err := generateAzureLinuxOciUri(imagecustomizerapi.AzureLinuxImage{ + Variant: "minimal-os", + Version: "2.0.20240425", + }) + assert.NoError(t, err) + assert.Equal(t, "mcr.microsoft.com/azurelinux/2.0/image/minimal-os:2.0.20240425", ociImage.Uri) +} + +func TestGenerateAzureLinuxOciUriAZL3Date(t *testing.T) { + ociImage, err := generateAzureLinuxOciUri(imagecustomizerapi.AzureLinuxImage{ + Variant: "minimal-os", + Version: "3.0.20250910", + }) + assert.NoError(t, err) + assert.Equal(t, "mcr.microsoft.com/azurelinux/3.0/image/minimal-os:3.0.20250910", ociImage.Uri) +} + +func TestCustomizeImageAZLBaseImageValid(t *testing.T) { + testutils.CheckSkipForCustomizeImageRequirements(t) + + testTmpDir := filepath.Join(tmpDir, "TestCustomizeImageAZLBaseImageValid") + defer os.RemoveAll(testTmpDir) + + buildDir := filepath.Join(testTmpDir, "build") + configFile := filepath.Join(testDir, "azurelinux-base-image.yaml") + outImageFilePath := filepath.Join(testTmpDir, "image.raw") + imageCacheDir := filepath.Join(testTmpDir, "image-cache") + + options := ImageCustomizerOptions{ + BuildDir: buildDir, + OutputImageFile: outImageFilePath, + OutputImageFormat: "raw", + UseBaseImageRpmRepos: true, + ImageCacheDir: imageCacheDir, + } + + // Customize image, with empty cache. + err := CustomizeImageWithConfigFileOptions(t.Context(), configFile, options) + if !assert.NoError(t, err) { + return + } + + imageConnection, err := connectToCoreEfiImage(buildDir, outImageFilePath) + if !assert.NoError(t, err) { + return + } + defer imageConnection.Close() + + // Ensure hostname was correctly set. + actualHostname, err := os.ReadFile(filepath.Join(imageConnection.Chroot().RootDir(), "etc/hostname")) + assert.NoError(t, err) + assert.Equal(t, "echidna", string(actualHostname)) +} diff --git a/toolkit/tools/pkg/imagecustomizerlib/imagecustomizer.go b/toolkit/tools/pkg/imagecustomizerlib/imagecustomizer.go index 768803d569..af6046c11f 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/imagecustomizer.go +++ b/toolkit/tools/pkg/imagecustomizerlib/imagecustomizer.go @@ -222,40 +222,39 @@ func CustomizeImageOptions(ctx context.Context, baseConfigPath string, config *i } }() - if rc.InputImageOci != nil { - ociImageFile, err := downloadOciImage(ctx, *rc.InputImageOci, options.BuildDir, - options.ImageCacheDir) - if err != nil { - return fmt.Errorf("%w:\n%w", ErrCustomizeDownloadImage, err) - } - - rc.InputImageFile = ociImageFile + // Ensure build and output folders are created up front + err = os.MkdirAll(rc.BuildDirAbs, os.ModePerm) + if err != nil { + return err } - err = ValidateConfigPostImageDownload(rc) + outputImageDir := filepath.Dir(rc.OutputImageFile) + err = os.MkdirAll(outputImageDir, os.ModePerm) if err != nil { - return fmt.Errorf("%w:\n%w", ErrInvalidImageConfig, err) + return err } - err = CheckEnvironmentVars() + // Download base image (if neccessary) + inputImageFilePath, err := downloadImage(ctx, rc.InputImage, options.BuildDir, + options.ImageCacheDir) if err != nil { - return err + return fmt.Errorf("%w:\n%w", ErrCustomizeDownloadImage, err) } - LogVersionsOfToolDeps() + rc.InputImage.Path = inputImageFilePath - // ensure build and output folders are created up front - err = os.MkdirAll(rc.BuildDirAbs, os.ModePerm) + err = ValidateConfigPostImageDownload(rc) if err != nil { - return err + return fmt.Errorf("%w:\n%w", ErrInvalidImageConfig, err) } - outputImageDir := filepath.Dir(rc.OutputImageFile) - err = os.MkdirAll(outputImageDir, os.ModePerm) + err = CheckEnvironmentVars() if err != nil { return err } + LogVersionsOfToolDeps() + inputIsoArtifacts, err := convertInputImageToWriteableFormat(ctx, rc) if err != nil { return fmt.Errorf("%w:\n%w", ErrConvertInputImage, err) @@ -330,10 +329,10 @@ func convertInputImageToWriteableFormat(ctx context.Context, rc *ResolvedConfig) defer span.End() if rc.InputIsIso() { - inputIsoArtifacts, err := createIsoArtifactStoreFromIsoImage(rc.InputImageFile, + inputIsoArtifacts, err := createIsoArtifactStoreFromIsoImage(rc.InputImage.Path, filepath.Join(rc.BuildDirAbs, "from-iso")) if err != nil { - return inputIsoArtifacts, fmt.Errorf("%w (source='%s'):\n%w", ErrCreateArtifactsStore, rc.InputImageFile, err) + return inputIsoArtifacts, fmt.Errorf("%w (source='%s'):\n%w", ErrCreateArtifactsStore, rc.InputImage.Path, err) } var liveosConfig LiveOSConfig @@ -365,7 +364,7 @@ func convertInputImageToWriteableFormat(ctx context.Context, rc *ResolvedConfig) } else { logger.Log.Infof("Creating raw base image: %s", rc.RawImageFile) - _, err := convertImageToRaw(rc.InputImageFile, rc.RawImageFile) + _, err := convertImageToRaw(rc.InputImage.Path, rc.RawImageFile) if err != nil { return nil, err } diff --git a/toolkit/tools/pkg/imagecustomizerlib/imagecustomizer_test.go b/toolkit/tools/pkg/imagecustomizerlib/imagecustomizer_test.go index 08736b44f6..2c7a6dea23 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/imagecustomizer_test.go +++ b/toolkit/tools/pkg/imagecustomizerlib/imagecustomizer_test.go @@ -850,7 +850,7 @@ func TestValidateConfig_InputImageFileSelection(t *testing.T) { // The input image file should be set to the value in the config. rc, err := ValidateConfig(t.Context(), configPath, config, false, options) assert.NoError(t, err) - assert.Equal(t, rc.InputImageFile, inputImageFileAsConfig) + assert.Equal(t, rc.InputImage.Path, inputImageFileAsConfig) assert.Equal(t, rc.InputFileExt(), "vhdx") assert.False(t, rc.InputIsIso()) @@ -861,7 +861,7 @@ func TestValidateConfig_InputImageFileSelection(t *testing.T) { // The input image file should be set to the value passed as an argument. rc, err = ValidateConfig(t.Context(), configPath, config, false, options) assert.NoError(t, err) - assert.Equal(t, rc.InputImageFile, inputImageFileAsArg) + assert.Equal(t, rc.InputImage.Path, inputImageFileAsArg) assert.Equal(t, rc.InputFileExt(), "vhdx") assert.False(t, rc.InputIsIso()) @@ -871,7 +871,7 @@ func TestValidateConfig_InputImageFileSelection(t *testing.T) { // The input image file should be set to the value passed as an argument. rc, err = ValidateConfig(t.Context(), configPath, config, false, options) assert.NoError(t, err) - assert.Equal(t, rc.InputImageFile, inputImageFileAsArg) + assert.Equal(t, rc.InputImage.Path, inputImageFileAsArg) assert.Equal(t, rc.InputFileExt(), "vhdx") assert.False(t, rc.InputIsIso()) @@ -881,7 +881,7 @@ func TestValidateConfig_InputImageFileSelection(t *testing.T) { options.OutputImageFile = "out/image.iso" rc, err = ValidateConfig(t.Context(), configPath, config, false, options) assert.NoError(t, err) - assert.Equal(t, rc.InputImageFile, inputImageFileIsoAsArg) + assert.Equal(t, rc.InputImage.Path, inputImageFileIsoAsArg) assert.Equal(t, rc.InputFileExt(), "iso") assert.True(t, rc.InputIsIso()) } diff --git a/toolkit/tools/pkg/imagecustomizerlib/notary.go b/toolkit/tools/pkg/imagecustomizerlib/notary.go new file mode 100644 index 0000000000..fe46327868 --- /dev/null +++ b/toolkit/tools/pkg/imagecustomizerlib/notary.go @@ -0,0 +1,154 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +package imagecustomizerlib + +import ( + "context" + "fmt" + "io/fs" + "os" + "path/filepath" + + "github.com/microsoft/azure-linux-image-tools/toolkit/tools/internal/file" + "github.com/microsoft/azure-linux-image-tools/toolkit/tools/internal/logger" + "github.com/notaryproject/notation-go" + "github.com/notaryproject/notation-go/dir" + "github.com/notaryproject/notation-go/registry" + "github.com/notaryproject/notation-go/verifier" + "github.com/notaryproject/notation-go/verifier/trustpolicy" + "github.com/notaryproject/notation-go/verifier/truststore" + ociv1 "github.com/opencontainers/image-spec/specs-go/v1" + orasregistry "oras.land/oras-go/v2/registry" + "oras.land/oras-go/v2/registry/remote" +) + +const ( + // The maximum number of singatures an OCI artifact is expected to have. + // This is mostly just a denial-of-service protection limit. + // So, the number is set to be unrealistically large. + maxOciSignatures = 50 +) + +type ociSignatureCheckOptions struct { + TrustPolicyName string + TrustStoreName string + CertificateFs fs.FS + CertificateFsPath string +} + +func checkNotationSignature(ctx context.Context, buildDir string, remoteRepo *remote.Repository, + descriptor ociv1.Descriptor, options ociSignatureCheckOptions, +) error { + // Recreate the full artifact reference URI for the provided digest. + digestUri := createOciDigestUri(remoteRepo.Reference, descriptor) + logger.Log.Debugf("Verifying OCI signature (%s)", digestUri) + + trustStorePath, err := os.MkdirTemp(buildDir, "trust-store-path-") + if err != nil { + return fmt.Errorf("failed to create OCI signature check certificate store:\n%w", err) + } + defer os.RemoveAll(trustStorePath) + + trustStoreType := truststore.TypeCA + trustStore, err := createNotationX509TrustStore(trustStorePath, options.TrustStoreName, trustStoreType, + options.CertificateFs, options.CertificateFsPath) + if err != nil { + return err + } + + verifier, err := createNotationX509Verifier(options.TrustPolicyName, options.TrustStoreName, trustStoreType, + trustStore) + if err != nil { + return err + } + + verifyOptions := notation.VerifyOptions{ + ArtifactReference: digestUri, + MaxSignatureAttempts: maxOciSignatures, + } + + notaryRepo := registry.NewRepository(remoteRepo) + + // Verify signature. + _, _, err = notation.Verify(ctx, verifier, notaryRepo, verifyOptions) + if err != nil { + return err + } + + return nil +} + +// Create the full digest URI of an OCI artifact. +func createOciDigestUri(registry orasregistry.Reference, artifact ociv1.Descriptor) string { + reference := registry + reference.Reference = artifact.Digest.String() + digestUri := reference.String() + return digestUri +} + +// Create a Notation trust store. +// This is mostly just a directory that contains the CA's public certificate. Though Notation has a specific directory +// layout that you have to follow. +func createNotationX509TrustStore(trustStorePath string, trustStoreName string, trustStoreType truststore.Type, + certificateFs fs.FS, certificateFsPath string, +) (truststore.X509TrustStore, error) { + // Create a directory to use as a trust store. + trustStoreFs := dir.NewSysFS(trustStorePath) + certDestinationDir, err := trustStoreFs.SysPath(dir.X509TrustStoreDir(string(trustStoreType), trustStoreName)) + if err != nil { + return nil, err + } + + certDestinationPath := filepath.Join(certDestinationDir, filepath.Base(certificateFsPath)) + + // Copy certificate into trust store. + err = file.CopyResourceFile(certificateFs, certificateFsPath, certDestinationPath, os.ModePerm, os.ModePerm) + if err != nil { + return nil, fmt.Errorf("failed to create OCI signature check certificate file (%s):\n%w", certDestinationPath, + err) + } + + // Create Notation trust store. + trustStore := truststore.NewX509TrustStore(trustStoreFs) + return trustStore, nil +} + +// Create a Notation verifier from an x509 trust store. +func createNotationX509Verifier(trustPolicyName string, trustStoreName string, trustStoreType truststore.Type, + trustStore truststore.X509TrustStore, +) (notation.Verifier, error) { + trustPolicy := &trustpolicy.Document{ + Version: "1.0", + TrustPolicies: []trustpolicy.TrustPolicy{ + { + Name: trustPolicyName, + RegistryScopes: []string{ + // Appply policy to all OCI artifacts. + "*", + }, + SignatureVerification: trustpolicy.SignatureVerification{ + VerificationLevel: "strict", + }, + TrustStores: []string{ + // The sub trust-stores to use in this policy. + string(trustStoreType) + ":" + trustStoreName, + }, + TrustedIdentities: []string{ + // The identities that are permitted in the sub trust-stores. + "*", + }, + }, + }, + } + + // Create verifier. + verifierOptions := verifier.VerifierOptions{} + + verifier, err := verifier.NewWithOptions(trustPolicy, trustStore, nil, verifierOptions) + if err != nil { + return nil, err + } + + return verifier, nil +} diff --git a/toolkit/tools/pkg/imagecustomizerlib/oras.go b/toolkit/tools/pkg/imagecustomizerlib/oras.go index 0681f1af84..9f8ef2b175 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/oras.go +++ b/toolkit/tools/pkg/imagecustomizerlib/oras.go @@ -31,7 +31,10 @@ var ( ) func downloadOciImage(ctx context.Context, ociImage imagecustomizerapi.OciImage, buildDir string, imageCacheDir string, + signatureCheckOptions *ociSignatureCheckOptions, ) (string, error) { + logger.Log.Debugf("Downloading OCI image (%s)", ociImage.Uri) + err := validateImageCacheDir(imageCacheDir) if err != nil { return "", err @@ -50,6 +53,13 @@ func downloadOciImage(ctx context.Context, ociImage imagecustomizerapi.OciImage, return "", err } + if signatureCheckOptions != nil { + err = checkNotationSignature(ctx, buildDir, remoteRepo, descriptor, *signatureCheckOptions) + if err != nil { + return "", fmt.Errorf("OCI signature check failed:\n%w", err) + } + } + digestsDir := filepath.Join(imageCacheDir, "digests", string(descriptor.Digest.Algorithm())) err = os.MkdirAll(digestsDir, os.ModePerm) @@ -65,7 +75,9 @@ func downloadOciImage(ctx context.Context, ociImage imagecustomizerapi.OciImage, return "", fmt.Errorf("failed to check if digest cache directory exists (%s):\n%w", digestDir, err) } - if !digestDirExists { + if digestDirExists { + logger.Log.Debugf("Using cached OCI image") + } else { err = downloadOciToDirectory(ctx, remoteRepo, digestDir, descriptor) if err != nil { return "", err diff --git a/toolkit/tools/pkg/imagecustomizerlib/oras_test.go b/toolkit/tools/pkg/imagecustomizerlib/oras_test.go index aa961bcffa..36b8c3c6bf 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/oras_test.go +++ b/toolkit/tools/pkg/imagecustomizerlib/oras_test.go @@ -85,4 +85,15 @@ func TestCustomizeImageOciBaseImageValid(t *testing.T) { logMessages = logMessagesHook.ConsumeMessages() assert.NotContains(t, logMessages, expectedDownloadLogMessage) + + imageConnection, err := connectToCoreEfiImage(buildDir, outImageFilePath) + if !assert.NoError(t, err) { + return + } + defer imageConnection.Close() + + // Ensure hostname was correctly set. + actualHostname, err := os.ReadFile(filepath.Join(imageConnection.Chroot().RootDir(), "etc/hostname")) + assert.NoError(t, err) + assert.Equal(t, "sugarglider", string(actualHostname)) } diff --git a/toolkit/tools/pkg/imagecustomizerlib/resolvedconfig.go b/toolkit/tools/pkg/imagecustomizerlib/resolvedconfig.go index 5045fb8c54..a6f3cfd851 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/resolvedconfig.go +++ b/toolkit/tools/pkg/imagecustomizerlib/resolvedconfig.go @@ -27,8 +27,7 @@ type ResolvedConfig struct { BuildDirAbs string // Input image - InputImageFile string - InputImageOci *imagecustomizerapi.OciImage + InputImage imagecustomizerapi.InputImage // Output artifacts OutputArtifacts *imagecustomizerapi.Artifacts @@ -48,7 +47,7 @@ type ResolvedConfig struct { } func (c *ResolvedConfig) InputFileExt() string { - fileExt := strings.TrimLeft(filepath.Ext(c.InputImageFile), ".") + fileExt := strings.TrimLeft(filepath.Ext(c.InputImage.Path), ".") return fileExt } diff --git a/toolkit/tools/pkg/imagecustomizerlib/testdata/azurelinux-base-image.yaml b/toolkit/tools/pkg/imagecustomizerlib/testdata/azurelinux-base-image.yaml new file mode 100644 index 0000000000..fd5175ba73 --- /dev/null +++ b/toolkit/tools/pkg/imagecustomizerlib/testdata/azurelinux-base-image.yaml @@ -0,0 +1,11 @@ +previewFeatures: +- input-image-oci + +input: + image: + azureLinux: + variant: minimal-os + version: 3.0 + +os: + hostname: echidna