diff --git a/cmd/skopeo/main.go b/cmd/skopeo/main.go index f15e5027..eea878ae 100644 --- a/cmd/skopeo/main.go +++ b/cmd/skopeo/main.go @@ -11,6 +11,7 @@ import ( "github.com/sirupsen/logrus" "github.com/spf13/cobra" commonFlag "go.podman.io/common/pkg/flag" + "go.podman.io/image/v5/pkg/cli/basetls/tlsdetails" "go.podman.io/image/v5/signature" "go.podman.io/image/v5/types" "go.podman.io/storage/pkg/reexec" @@ -21,6 +22,7 @@ var defaultUserAgent = "skopeo/" + version.Version type globalOptions struct { debug bool // Enable debug output tlsVerify commonFlag.OptionalBool // Require HTTPS and verify certificates (for docker: and docker-daemon:) + tlsDetailsPath string // Path to a containers-tls-details.yaml(5) file policyPath string // Path to a signature verification policy file insecurePolicy bool // Use an "allow everything" signature verification policy registriesDirPath string // Path to a "registries.d" registry configuration directory @@ -80,6 +82,7 @@ func createApp() (*cobra.Command, *globalOptions) { var dummyVersion bool rootCommand.Flags().BoolVarP(&dummyVersion, "version", "v", false, "Version for Skopeo") rootCommand.PersistentFlags().BoolVar(&opts.debug, "debug", false, "enable debug output") + rootCommand.PersistentFlags().StringVar(&opts.tlsDetailsPath, "tls-details", "", "path to a containers-tls-details.yaml(5) file") rootCommand.PersistentFlags().StringVar(&opts.policyPath, "policy", "", "Path to a trust policy file") rootCommand.PersistentFlags().BoolVar(&opts.insecurePolicy, "insecure-policy", false, "run the tool without any policy check") rootCommand.PersistentFlags().BoolVar(&opts.requireSigned, "require-signed", false, "require any pulled image to be signed") @@ -208,6 +211,10 @@ func (opts *globalOptions) newSystemContext() (*types.SystemContext, error) { if opts.userAgentPrefix != "" { userAgent = opts.userAgentPrefix + " " + defaultUserAgent } + baseTLSConfig, err := tlsdetails.BaseTLSFromOptionalFile(opts.tlsDetailsPath) + if err != nil { + return nil, err + } ctx := &types.SystemContext{ RegistriesDirPath: opts.registriesDirPath, ArchitectureChoice: opts.overrideArch, @@ -215,6 +222,7 @@ func (opts *globalOptions) newSystemContext() (*types.SystemContext, error) { VariantChoice: opts.overrideVariant, SystemRegistriesConfPath: opts.registriesConfPath, BigFilesTemporaryDir: opts.tmpDir, + BaseTLSConfig: baseTLSConfig.TLSConfig(), DockerRegistryUserAgent: userAgent, } // DEPRECATED: We support this for backward compatibility, but override it if a per-image flag is provided. diff --git a/cmd/skopeo/main_test.go b/cmd/skopeo/main_test.go index dd9ea6ef..0576af97 100644 --- a/cmd/skopeo/main_test.go +++ b/cmd/skopeo/main_test.go @@ -2,6 +2,7 @@ package main import ( "bytes" + "crypto/tls" "testing" "github.com/stretchr/testify/assert" @@ -35,6 +36,7 @@ func TestGlobalOptionsNewSystemContext(t *testing.T) { "--override-arch", "overridden-arch", "--override-os", "overridden-os", "--override-variant", "overridden-variant", + "--tls-details", "../../integration/fixtures/tls-details-pqc-only.yaml", "--tmpdir", "/srv", "--registries-conf", "/srv/registries.conf", "--tls-verify=false", @@ -42,10 +44,14 @@ func TestGlobalOptionsNewSystemContext(t *testing.T) { res, err = opts.newSystemContext() require.NoError(t, err) assert.Equal(t, &types.SystemContext{ - RegistriesDirPath: "/srv/registries.d", - ArchitectureChoice: "overridden-arch", - OSChoice: "overridden-os", - VariantChoice: "overridden-variant", + RegistriesDirPath: "/srv/registries.d", + ArchitectureChoice: "overridden-arch", + OSChoice: "overridden-os", + VariantChoice: "overridden-variant", + BaseTLSConfig: &tls.Config{ + MinVersion: tls.VersionTLS13, + CurvePreferences: []tls.CurveID{tls.X25519MLKEM768}, + }, BigFilesTemporaryDir: "/srv", SystemRegistriesConfPath: "/srv/registries.conf", DockerInsecureSkipTLSVerify: types.OptionalBoolTrue, diff --git a/docs/skopeo.1.md b/docs/skopeo.1.md index d78fefcb..d7fd8f04 100644 --- a/docs/skopeo.1.md +++ b/docs/skopeo.1.md @@ -96,6 +96,15 @@ Use registry configuration files in _dir_ (e.g. for container signature storage) Require that any pulled image must be signed regardless of what the default or provided trust policy file says. +**--tls-details** _path_ + +Path to a containers-tls-details(5) file, affecting TLS behavior throughout the program. + +If not set, defaults to a reasonable default that may change over time (depending on system’s global policy, +version of the program, version of the Go language, and the like). + +Users should generally not use this option unless they have a process to ensure that the configuration will be kept up to date. + **--tmpdir** _dir_ Directory used to store temporary files. Defaults to /var/tmp. diff --git a/integration/fixtures/tls-details-1.3.yaml b/integration/fixtures/tls-details-1.3.yaml new file mode 100644 index 00000000..7f1c6e86 --- /dev/null +++ b/integration/fixtures/tls-details-1.3.yaml @@ -0,0 +1 @@ +minVersion: "1.3" diff --git a/integration/fixtures/tls-details-anything.yaml b/integration/fixtures/tls-details-anything.yaml new file mode 100644 index 00000000..f6105696 --- /dev/null +++ b/integration/fixtures/tls-details-anything.yaml @@ -0,0 +1 @@ +{} # No fields diff --git a/integration/fixtures/tls-details-pqc-only.yaml b/integration/fixtures/tls-details-pqc-only.yaml new file mode 100644 index 00000000..7f953601 --- /dev/null +++ b/integration/fixtures/tls-details-pqc-only.yaml @@ -0,0 +1,3 @@ +minVersion: "1.3" +namedGroups: + - "X25519MLKEM768" diff --git a/integration/tls_test.go b/integration/tls_test.go new file mode 100644 index 00000000..59946fe0 --- /dev/null +++ b/integration/tls_test.go @@ -0,0 +1,302 @@ +package main + +import ( + "context" + "crypto/rand" + "crypto/rsa" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "encoding/json" + "encoding/pem" + "fmt" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "testing" + "time" + + "github.com/opencontainers/image-spec/specs-go" + imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" + "go.podman.io/image/v5/oci/layout" +) + +func TestTLS(t *testing.T) { + suite.Run(t, &tlsSuite{}) +} + +type tlsSuite struct { + suite.Suite + defaultServer *tlsConfigServer + tls12Server *tlsConfigServer + nonPQCserver *tlsConfigServer + pqcServer *tlsConfigServer + + expected []expectedBehavior +} + +var ( + _ = suite.SetupAllSuite(&tlsSuite{}) + _ = suite.TearDownAllSuite(&tlsSuite{}) +) + +type expectedBehavior struct { + server *tlsConfigServer + tlsDetails string + expected string +} + +func (s *tlsSuite) SetupSuite() { + t := s.T() + + s.defaultServer = newServer(t, &tls.Config{}) + s.tls12Server = newServer(t, &tls.Config{ + MaxVersion: tls.VersionTLS12, + }) + s.nonPQCserver = newServer(t, &tls.Config{ + MinVersion: tls.VersionTLS13, + CurvePreferences: []tls.CurveID{tls.X25519, tls.CurveP256, tls.CurveP384, tls.CurveP521}, + }) + s.pqcServer = newServer(t, &tls.Config{ + MinVersion: tls.VersionTLS13, + CurvePreferences: []tls.CurveID{tls.X25519MLKEM768}, + }) + + s.expected = []expectedBehavior{ + { + server: s.defaultServer, + tlsDetails: "fixtures/tls-details-anything.yaml", + expected: `\b418\b`, // "I'm a teapot" + }, + { + server: s.tls12Server, + tlsDetails: "fixtures/tls-details-anything.yaml", + expected: `\b418\b`, // "I'm a teapot" + }, + { + server: s.nonPQCserver, + tlsDetails: "fixtures/tls-details-anything.yaml", + expected: `\b418\b`, // "I'm a teapot" + }, + { + server: s.pqcServer, + tlsDetails: "fixtures/tls-details-anything.yaml", + expected: `\b418\b`, // "I'm a teapot" + }, + + { + server: s.defaultServer, + tlsDetails: "fixtures/tls-details-1.3.yaml", + expected: `\b418\b`, // "I'm a teapot" + }, + { + server: s.tls12Server, + tlsDetails: "fixtures/tls-details-1.3.yaml", + expected: `protocol version not supported`, + }, + { + server: s.nonPQCserver, + tlsDetails: "fixtures/tls-details-1.3.yaml", + expected: `\b418\b`, // "I'm a teapot" + }, + { + server: s.pqcServer, + tlsDetails: "fixtures/tls-details-1.3.yaml", + expected: `\b418\b`, // "I'm a teapot" + }, + + { + server: s.defaultServer, + tlsDetails: "fixtures/tls-details-pqc-only.yaml", + expected: `\b418\b`, // "I'm a teapot" + }, + { + server: s.tls12Server, + tlsDetails: "fixtures/tls-details-pqc-only.yaml", + expected: `protocol version not supported`, + }, + { + server: s.nonPQCserver, + tlsDetails: "fixtures/tls-details-pqc-only.yaml", + expected: `handshake failure`, + }, + { + server: s.pqcServer, + tlsDetails: "fixtures/tls-details-pqc-only.yaml", + expected: `\b418\b`, // "I'm a teapot" + }, + } +} + +func (s *tlsSuite) TearDownSuite() { +} + +func (s *tlsSuite) TestDockerDaemon() { + t := s.T() + + // Our server doesn’t perform client authentication, but the docker-daemon: option semantics + // requires us to provide a certificate if we want to specify a CA. + dockerCertPath := t.TempDir() + caPath := filepath.Join(dockerCertPath, "ca.pem") + + privateKey, err := rsa.GenerateKey(rand.Reader, 4096) + require.NoError(t, err) + publicKey := &privateKey.PublicKey + + err = os.WriteFile(filepath.Join(dockerCertPath, "key.pem"), pem.EncodeToMemory(&pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(privateKey), + }), 0o644) + require.NoError(t, err) + + referenceTime := time.Now() + template := &x509.Certificate{ + Subject: pkix.Name{ + CommonName: "client", + }, + NotBefore: referenceTime.Add(-1 * time.Minute), + NotAfter: referenceTime.Add(1 * time.Hour), + } + certDER, err := x509.CreateCertificate(rand.Reader, template, template, publicKey, privateKey) + require.NoError(t, err) + err = os.WriteFile(filepath.Join(dockerCertPath, "cert.pem"), pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", + Bytes: certDER, + }), 0o644) + require.NoError(t, err) + + for _, e := range s.expected { + err := os.WriteFile(caPath, e.server.certBytes, 0o644) + require.NoError(t, err) + assertSkopeoFails(t, e.expected, "--tls-details", e.tlsDetails, "inspect", "--daemon-host", e.server.server.URL, "--cert-dir", dockerCertPath, "docker-daemon:repo:tag") + } +} + +func (s *tlsSuite) TestRegistry() { + t := s.T() + + caDir := t.TempDir() + caPath := filepath.Join(caDir, "ca.crt") + + for _, e := range s.expected { + err := os.WriteFile(caPath, e.server.certBytes, 0o644) + require.NoError(t, err) + assertSkopeoFails(t, e.expected, "--tls-details", e.tlsDetails, "inspect", "--cert-dir", caDir, "docker://"+e.server.hostPort+"/repo") + } +} + +func (s *tlsSuite) TestOCILayout() { + t := s.T() + + caDir := t.TempDir() + caPath := filepath.Join(caDir, "ca.crt") + + for _, e := range s.expected { + err := os.WriteFile(caPath, e.server.certBytes, 0o644) + require.NoError(t, err) + + ociLayoutDir := t.TempDir() + destRef, err := layout.NewReference(ociLayoutDir, "repo:tag") + require.NoError(t, err) + dest, err := destRef.NewImageDestination(context.Background(), nil) + require.NoError(t, err) + manifestBytes, err := json.Marshal(imgspecv1.Manifest{ + Versioned: specs.Versioned{SchemaVersion: 2}, + MediaType: imgspecv1.MediaTypeImageManifest, + Config: imgspecv1.Descriptor{ + MediaType: imgspecv1.MediaTypeImageConfig, + Digest: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + Size: 42, + URLs: []string{e.server.server.URL + "/config.json"}, + }, + Layers: []imgspecv1.Descriptor{}, + ArtifactType: "", + Subject: &imgspecv1.Descriptor{}, + Annotations: map[string]string{}, + }) + require.NoError(t, err) + err = dest.PutManifest(context.Background(), manifestBytes, nil) + require.NoError(t, err) + err = dest.Commit(context.Background(), nil) // nil is technically invalid, but works here + require.NoError(t, err) + err = dest.Close() + require.NoError(t, err) + + // We don’t expose types.OCICertPath in the CLI. But if we get far enough to be worrying about certificates, + // we already negotiated the TLS version and named group. + expected := e.expected + if expected == `\b418\b` { + expected = `certificate signed by unknown authority` + } + assertSkopeoFails(t, expected, "--tls-details", e.tlsDetails, "inspect", "oci:"+ociLayoutDir) + } +} + +func (s *tlsSuite) TestOpenShift() { + t := s.T() + + configDir := t.TempDir() + configPath := filepath.Join(configDir, "kubeconfig") + t.Setenv("KUBECONFIG", configPath) + + for _, e := range s.expected { + err := os.WriteFile(configPath, []byte(fmt.Sprintf( + `apiVersion: v1 +clusters: +- cluster: + certificate-authority: "%s" + server: "%s" + name: our-cluster +contexts: +- context: + cluster: our-cluster + namespace: default + name: our-context +current-context: our-context +kind: Config +`, e.server.certPath, e.server.server.URL)), 0o644) + require.NoError(t, err) + // The atomic: image access starts with resolving the tag in a k8s API (and that will always fail, one way or another), + // so we never actually contact registry.example. + assertSkopeoFails(t, e.expected, "--tls-details", e.tlsDetails, "inspect", "atomic:registry.example/namespace/repo:tag") + } +} + +// tlsConfigServer serves TLS with a specific configuration. +// It returns StatusTeapot on all requests; we use that to detect that the TLS negotiation succeeded, +// without bothering to actually implement any of the protocols. +type tlsConfigServer struct { + server *httptest.Server + hostPort string + certBytes []byte + certPath string +} + +func newServer(t *testing.T, config *tls.Config) *tlsConfigServer { + server := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusTeapot) + })) + t.Cleanup(server.Close) + + server.TLS = config.Clone() + server.StartTLS() + + certBytes := pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", + Bytes: server.Certificate().Raw, + }) + certDir := t.TempDir() + certPath := filepath.Join(certDir, "cert.pem") + err := os.WriteFile(certPath, certBytes, 0o644) + require.NoError(t, err) + + return &tlsConfigServer{ + server: server, + hostPort: server.Listener.Addr().String(), + certBytes: certBytes, + certPath: certPath, + } +} diff --git a/vendor/go.podman.io/image/v5/pkg/cli/basetls/basetls.go b/vendor/go.podman.io/image/v5/pkg/cli/basetls/basetls.go new file mode 100644 index 00000000..1558e8f4 --- /dev/null +++ b/vendor/go.podman.io/image/v5/pkg/cli/basetls/basetls.go @@ -0,0 +1,219 @@ +// Package basetls encapsulates a set of base TLS settings (not keys/certificates) +// configured via containers-tls-details.yaml(5). +// +// CLI integration should generally be done using c/image/pkg/cli/basetls/tlsdetails instead +// of using the TLSDetailsFile directly. +package basetls + +import ( + "bytes" + "crypto/tls" + "encoding/json" + "errors" + "fmt" + "slices" +) + +// Config encapsulates user’s choices about base TLS settings, typically +// configured via containers-tls-details.yaml(5). +// +// Most codebases should pass around the resulting *tls.Config, without depending on this subpackage; +// this primarily exists as a separate type to allow passing the configuration around within (version-matched) RPC systems, +// using the MarshalText/UnmarshalText methods. +type Config struct { + // We keep the text representation because we start with it, and this way we don't have + // to implement formatting back to text. This is an internal detail, so we can change that later. + text TLSDetailsFile + config *tls.Config // Parsed from .text, both match +} + +// TLSDetailsFile contains a set of TLS options. +// +// To consume such a file, most callers should use c/image/pkg/cli/basetls/tlsdetails instead +// of dealing with this type explicitly. +// +// This type is exported primarily to allow creating parameter files programmatically +// (and eventually the tlsdetails subpackage should provide an API to convert this type into +// the appropriate file contents, so that callers don't need to do that manually). +type TLSDetailsFile struct { + // Keep this in sync with docs/containers-tls-details.yaml.5.md ! + + MinVersion string `yaml:"minVersion,omitempty"` // If set, minimum version to use throughout the program. + CipherSuites []string `yaml:"cipherSuites,omitempty"` // If set, allowed TLS cipher suites to use throughout the program. + NamedGroups []string `yaml:"namedGroups,omitempty"` // If set, allowed TLS named groups to use throughout the program. +} + +// NewFromTLSDetails creates a Config from a TLSDetailsFile. +func NewFromTLSDetails(details *TLSDetailsFile) (*Config, error) { + res := Config{ + text: TLSDetailsFile{}, + config: &tls.Config{}, + } + configChanged := false + for _, fn := range []func(input *TLSDetailsFile) (bool, error){ + res.parseMinVersion, + res.parseCipherSuites, + res.parseNamedGroups, + } { + changed, err := fn(details) + if err != nil { + return nil, err + } + if changed { + configChanged = true + } + } + + if !configChanged { + res.config = nil + } + return &res, nil +} + +// tlsVersions maps TLS version strings to their crypto/tls constants. +// We could use the `tls.VersionName` names, but those are verbose and contain spaces; +// similarly the OpenShift enum values (“VersionTLS11”) are unergonomic. +var tlsVersions = map[string]uint16{ + "1.0": tls.VersionTLS10, + "1.1": tls.VersionTLS11, + "1.2": tls.VersionTLS12, + "1.3": tls.VersionTLS13, +} + +func (c *Config) parseMinVersion(input *TLSDetailsFile) (bool, error) { + if input.MinVersion == "" { + return false, nil + } + v, ok := tlsVersions[input.MinVersion] + if !ok { + return false, fmt.Errorf("unrecognized TLS minimum version %q", input.MinVersion) + } + c.text.MinVersion = input.MinVersion + c.config.MinVersion = v + return true, nil +} + +// cipherSuitesByName returns a map from cipher suite name to its ID. +func cipherSuitesByName() map[string]uint16 { + // The Go standard library uses IANA names and already contains the mapping (for relevant values) + // sadly we still need to turn it into a lookup map. + suites := make(map[string]uint16) + for _, cs := range tls.CipherSuites() { + suites[cs.Name] = cs.ID + } + for _, cs := range tls.InsecureCipherSuites() { + suites[cs.Name] = cs.ID + } + return suites +} + +func (c *Config) parseCipherSuites(input *TLSDetailsFile) (bool, error) { + if input.CipherSuites == nil { + return false, nil + } + suitesByName := cipherSuitesByName() + ids := []uint16{} + for _, name := range input.CipherSuites { + id, ok := suitesByName[name] + if !ok { + return false, fmt.Errorf("unrecognized TLS cipher suite %q", name) + } + ids = append(ids, id) + } + c.text.CipherSuites = slices.Clone(input.CipherSuites) + c.config.CipherSuites = ids + return true, nil +} + +// groupsByName maps curve/group names to their tls.CurveID. +// The names match IANA TLS Supported Groups registry. +// +// Yes, the x25519 names differ in capitalization. +// Go’s tls.CurveID has a .String() method, but it +// uses the Go names. +var groupsByName = map[string]tls.CurveID{ + "secp256r1": tls.CurveP256, + "secp384r1": tls.CurveP384, + "secp521r1": tls.CurveP521, + "x25519": tls.X25519, + "X25519MLKEM768": tls.X25519MLKEM768, +} + +func (c *Config) parseNamedGroups(input *TLSDetailsFile) (bool, error) { + if input.NamedGroups == nil { + return false, nil + } + ids := []tls.CurveID{} + for _, name := range input.NamedGroups { + id, ok := groupsByName[name] + if !ok { + return false, fmt.Errorf("unrecognized TLS named group %q", name) + } + ids = append(ids, id) + } + c.text.NamedGroups = slices.Clone(input.NamedGroups) + c.config.CurvePreferences = ids + return true, nil +} + +// TLSConfig returns a *tls.Config matching the provided settings. +// If c contains no settings, it returns nil. +// Otherwise, the returned *tls.Config is freshly allocated and the caller can modify it as needed. +func (c *Config) TLSConfig() *tls.Config { + if c.config == nil { + return nil + } + return c.config.Clone() +} + +// marshaledSerialization is the data we use in MarshalText/UnmarshalText, +// marshaled using JSON. +// +// Note that the file format is using YAML, but we use JSON, to minimize dependencies +// in backend code where we don't need comments and the brackets are not annoying users. +type marshaledSerialization struct { + Version int + Data TLSDetailsFile +} + +const marshaledSerializationVersion1 = 1 + +// MarshalText serializes c to a text representation. +// +// The representation is intended to be reasonably stable across updates to c/image, +// but the consumer must not be older than the producer. +func (c Config) MarshalText() ([]byte, error) { + data := marshaledSerialization{ + Version: marshaledSerializationVersion1, + Data: c.text, + } + return json.Marshal(data) +} + +// UnmarshalText parses the output of MarshalText. +// +// The format is otherwise undocumented and we do not promise ongoing compatibility with producers external to this package. +func (c *Config) UnmarshalText(text []byte) error { + var data marshaledSerialization + + // In the future, this should be an even stricter parser, e.g. refusing duplicate fields + // and requiring a case-sensitive field name match. + decoder := json.NewDecoder(bytes.NewReader(text)) + decoder.DisallowUnknownFields() + if err := decoder.Decode(&data); err != nil { + return err + } + if decoder.More() { + return errors.New("unexpected extra data after a JSON object") + } + + if data.Version != marshaledSerializationVersion1 { + return fmt.Errorf("unsupported version %d", data.Version) + } + v, err := NewFromTLSDetails(&data.Data) + if err != nil { + return err + } + *c = *v + return nil +} diff --git a/vendor/go.podman.io/image/v5/pkg/cli/basetls/tlsdetails/tlsdetails.go b/vendor/go.podman.io/image/v5/pkg/cli/basetls/tlsdetails/tlsdetails.go new file mode 100644 index 00000000..fca6bed9 --- /dev/null +++ b/vendor/go.podman.io/image/v5/pkg/cli/basetls/tlsdetails/tlsdetails.go @@ -0,0 +1,59 @@ +// Package tlsdetails implements the containers-tls-details.yaml(5) file format. +// +// Recommended CLI integration is by a --tls-details flag parsed using BaseTLSFromOptionalFile, with the following documentation: +// +// --tls-details is a path to a containers-tls-details.yaml(5) file, affecting TLS behavior throughout the program. +// +// If not set, defaults to a reasonable default that may change over time (depending on system’s global policy, +// version of the program, version of the Go language, and the like). +// +// Users should generally not use this option unless they have a process to ensure that the configuration will be kept up to date. +package tlsdetails + +import ( + "bytes" + "fmt" + "os" + + "go.podman.io/image/v5/pkg/cli/basetls" + "gopkg.in/yaml.v3" +) + +// BaseTLSFromOptionalFile returns a basetls.Config matching a containers-tls-details.yaml file at the specified path. +// If path is "", it returns a valid basetls.Config with no settings (where config.TLSConfig() will return nil). +func BaseTLSFromOptionalFile(path string) (*basetls.Config, error) { + if path == "" { + return basetls.NewFromTLSDetails(&basetls.TLSDetailsFile{}) + } + return BaseTLSFromFile(path) +} + +// BaseTLSFromFile returns a basetls.Config matching a containers-tls-details.yaml file at the specified path. +func BaseTLSFromFile(path string) (*basetls.Config, error) { + details, err := ParseFile(path) + if err != nil { + return nil, err + } + res, err := basetls.NewFromTLSDetails(details) + if err != nil { + return nil, fmt.Errorf("parsing TLS details %q: %w", path, err) + } + return res, nil +} + +// ParseFile parses a basetls.TLSDetailsFile at the specified path. +// +// Most consumers of the parameter file should use BaseTLSFromFile or BaseTLSFromOptionalFile instead. +func ParseFile(path string) (*basetls.TLSDetailsFile, error) { + var res basetls.TLSDetailsFile + source, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("reading %q: %w", path, err) + } + dec := yaml.NewDecoder(bytes.NewReader(source)) + dec.KnownFields(true) + if err = dec.Decode(&res); err != nil { + return nil, fmt.Errorf("parsing %q: %w", path, err) + } + return &res, nil +} diff --git a/vendor/modules.txt b/vendor/modules.txt index c3a5bd40..76306d31 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -410,6 +410,8 @@ go.podman.io/image/v5/pkg/blobinfocache/memory go.podman.io/image/v5/pkg/blobinfocache/none go.podman.io/image/v5/pkg/blobinfocache/sqlite go.podman.io/image/v5/pkg/cli +go.podman.io/image/v5/pkg/cli/basetls +go.podman.io/image/v5/pkg/cli/basetls/tlsdetails go.podman.io/image/v5/pkg/cli/sigstore go.podman.io/image/v5/pkg/cli/sigstore/params go.podman.io/image/v5/pkg/compression