mirror of
https://github.com/containers/skopeo.git
synced 2026-04-22 15:47:23 +08:00
Add a --tls-details option and integration tests
Signed-off-by: Miloslav Trmač <mitr@redhat.com>
This commit is contained in:
@@ -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.
|
||||
|
||||
+10
-4
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
minVersion: "1.3"
|
||||
@@ -0,0 +1 @@
|
||||
{} # No fields
|
||||
@@ -0,0 +1,3 @@
|
||||
minVersion: "1.3"
|
||||
namedGroups:
|
||||
- "X25519MLKEM768"
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
+219
@@ -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
|
||||
}
|
||||
+59
@@ -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
|
||||
}
|
||||
Vendored
+2
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user