Skip to content

Commit 789ace0

Browse files
committed
Add image/pkg/cli/basetls
Signed-off-by: Miloslav Trmač <mitr@redhat.com>
1 parent 935e8b4 commit 789ace0

9 files changed

Lines changed: 631 additions & 0 deletions

File tree

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
% CONTAINERS-TLS-DETAILS.YAML 5 container-libs TLS details file format
2+
% Miloslav Trmač
3+
% February 2026
4+
5+
# NAME
6+
containers-tls-details.yaml - syntax for the container-libs TLS details parameter file
7+
8+
# DESCRIPTION
9+
10+
The TLS details parameter file is accepted by various projects using the go.podman.io/* libraries.
11+
There is no default location for these files; they are user-managed, and a path is provided on the CLI,
12+
e.g. `skopeo --tls-details=`_details-file_`.yaml copy …`.
13+
14+
# WARNINGS
15+
16+
The `--tls-details` options, and this file format, should only rarely be used.
17+
If this mechanism is not used, the software is expected to use appropriate defaults which will vary over time,
18+
depending on version of the software, version of the Go standard library,
19+
or platform’s configuration (e.g. `GODEBUG` values; or, not as of early 2026, but potentially, **crypto-policies**(7)).
20+
21+
These options _only_ affect the programs which provide the `--tls-details` option;
22+
they don't affect other executables (e.g. **git**(1), **ssh**(1)) that may be executed internally to perform another operation.
23+
24+
There are some known gaps in the implementation of these options.
25+
We hope to fix that over time, but in the meantime, careful testing feature by feature is recommended.
26+
Known gaps include network operations performed while creating sigstore signatures (communicating with Rekor, OIDC servers, Fulcio).
27+
28+
# FORMAT
29+
30+
The TLS details files use YAML. All fields are optional.
31+
32+
- `minVersion`
33+
34+
The minimum TLS version to use throughout the program.
35+
If not set, defaults to a reasonable default that may change over time.
36+
37+
Users should generally not use this option and hard-code a version unless they have a process
38+
to ensure that the value will be kept up to date.
39+
40+
- `cipherSuites`
41+
42+
The allowed TLS cipher suites to use throughout the program.
43+
The value is an array of IANA TLS Cipher Suites names.
44+
45+
If not set, defaults to a reasonable default that may change over time;
46+
if set to an empty array, prohibits using all cipher suites.
47+
48+
**Warning:** Almost no-one should ever use this option.
49+
Use it only if you have a bureaucracy that requires a specific list,
50+
and if you are confident that this bureaucracy will still exist,
51+
and will bring you an updated list when necessary,
52+
many years from now.
53+
54+
**Warning:** The effectiveness of this option is limited by capabilities of the Go standard library;
55+
e.g., as of Go 1.25, it is not possible to change which cipher suites are used in TLS 1.3.
56+
57+
- `namedGroups`
58+
59+
The allowed TLS named groups to use throughout the program.
60+
The value is an array of IANA TLS Supported Groups names.
61+
62+
If not set, defaults to a reasonable default that may change over time.
63+
64+
**Warning:** Almost no-one should ever use this option.
65+
Use it only if you have a bureaucracy that requires a specific list,
66+
and if you are confident that this bureaucracy will still exist,
67+
and will bring you an updated list when necessary,
68+
many years from now.
69+
70+
# EXAMPLE
71+
72+
```yaml
73+
minVersion: "1.2"
74+
cipherSuites:
75+
- "TLS_AES_128_GCM_SHA256"
76+
- "TLS_CHACHA20_POLY1305_SHA256"
77+
namedGroups:
78+
- "secp256r1"
79+
- "secp384r1"
80+
- "x25519"
81+
```
82+
83+
# SEE ALSO
84+
buildah(1), podman(1), skopeo(1)

image/pkg/cli/basetls/basetls.go

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
// Package basetls encapsulates a set of base TLS settings (not keys/certificates)
2+
// configured via containers-tls-details.yaml(5).
3+
//
4+
// CLI integration should generally be done using c/image/pkg/cli/basetls/tlsdetails instead
5+
// of using the TLSDetailsFile directly.
6+
package basetls
7+
8+
import (
9+
"bytes"
10+
"crypto/tls"
11+
"encoding/json"
12+
"errors"
13+
"fmt"
14+
"slices"
15+
)
16+
17+
// Config encapsulates user’s choices about base TLS settings, typically
18+
// configured via containers-tls-details.yaml(5).
19+
//
20+
// Most codebases should pass around the resulting *tls.Config, without depending on this subpackage;
21+
// this primarily exists as a separate type to allow passing the configuration around within (version-matched) RPC systems,
22+
// using the MarshalText/UnmarshalText methods.
23+
type Config struct {
24+
// We keep the text representation because we start with it, and this way we don't have
25+
// to implement formatting back to text. This is an internal detail, so we can change that later.
26+
text TLSDetailsFile
27+
config *tls.Config // Parsed from .text, both match
28+
}
29+
30+
// TLSDetailsFile contains a set of TLS options.
31+
//
32+
// To consume such a file, most callers should use c/image/pkg/cli/basetls/tlsdetails instead
33+
// of dealing with this type explicitly.
34+
//
35+
// This type is exported primarily to allow creating parameter files programmatically
36+
// (and eventually the tlsdetails subpackage should provide an API to convert this type into
37+
// the appropriate file contents, so that callers don't need to do that manually).
38+
type TLSDetailsFile struct {
39+
// Keep this in sync with docs/containers-tls-details.yaml.5.md !
40+
41+
MinVersion string `yaml:"minVersion,omitempty"` // If set, minimum version to use throughout the program.
42+
CipherSuites []string `yaml:"cipherSuites,omitempty"` // If set, allowed TLS cipher suites to use throughout the program.
43+
NamedGroups []string `yaml:"namedGroups,omitempty"` // If set, allowed TLS named groups to use throughout the program.
44+
}
45+
46+
// NewFromTLSDetails creates a Config from a TLSDetailsFile.
47+
func NewFromTLSDetails(details *TLSDetailsFile) (*Config, error) {
48+
res := Config{
49+
text: TLSDetailsFile{},
50+
config: &tls.Config{},
51+
}
52+
configChanged := false
53+
for _, fn := range []func(input *TLSDetailsFile) (bool, error){
54+
res.parseMinVersion,
55+
res.parseCipherSuites,
56+
res.parseNamedGroups,
57+
} {
58+
changed, err := fn(details)
59+
if err != nil {
60+
return nil, err
61+
}
62+
if changed {
63+
configChanged = true
64+
}
65+
}
66+
67+
if !configChanged {
68+
res.config = nil
69+
}
70+
return &res, nil
71+
}
72+
73+
// tlsVersions maps TLS version strings to their crypto/tls constants.
74+
// We could use the `tls.VersionName` names, but those are verbose and contain spaces;
75+
// similarly the OpenShift enum values (“VersionTLS11”) are unergonomic.
76+
var tlsVersions = map[string]uint16{
77+
"1.0": tls.VersionTLS10,
78+
"1.1": tls.VersionTLS11,
79+
"1.2": tls.VersionTLS12,
80+
"1.3": tls.VersionTLS13,
81+
}
82+
83+
func (c *Config) parseMinVersion(input *TLSDetailsFile) (bool, error) {
84+
if input.MinVersion == "" {
85+
return false, nil
86+
}
87+
v, ok := tlsVersions[input.MinVersion]
88+
if !ok {
89+
return false, fmt.Errorf("unrecognized TLS minimum version %q", input.MinVersion)
90+
}
91+
c.text.MinVersion = input.MinVersion
92+
c.config.MinVersion = v
93+
return true, nil
94+
}
95+
96+
// cipherSuitesByName returns a map from cipher suite name to its ID.
97+
func cipherSuitesByName() map[string]uint16 {
98+
// The Go standard library uses IANA names and already contains the mapping (for relevant values)
99+
// sadly we still need to turn it into a lookup map.
100+
suites := make(map[string]uint16)
101+
for _, cs := range tls.CipherSuites() {
102+
suites[cs.Name] = cs.ID
103+
}
104+
for _, cs := range tls.InsecureCipherSuites() {
105+
suites[cs.Name] = cs.ID
106+
}
107+
return suites
108+
}
109+
110+
func (c *Config) parseCipherSuites(input *TLSDetailsFile) (bool, error) {
111+
if input.CipherSuites == nil {
112+
return false, nil
113+
}
114+
suitesByName := cipherSuitesByName()
115+
ids := []uint16{}
116+
for _, name := range input.CipherSuites {
117+
id, ok := suitesByName[name]
118+
if !ok {
119+
return false, fmt.Errorf("unrecognized TLS cipher suite %q", name)
120+
}
121+
ids = append(ids, id)
122+
}
123+
c.text.CipherSuites = slices.Clone(input.CipherSuites)
124+
c.config.CipherSuites = ids
125+
return true, nil
126+
}
127+
128+
// groupsByName maps curve/group names to their tls.CurveID.
129+
// The names match IANA TLS Supported Groups registry.
130+
//
131+
// Yes, the x25519 names differ in capitalization.
132+
// Go’s tls.CurveID has a .String() method, but it
133+
// uses the Go names.
134+
var groupsByName = map[string]tls.CurveID{
135+
"secp256r1": tls.CurveP256,
136+
"secp384r1": tls.CurveP384,
137+
"secp521r1": tls.CurveP521,
138+
"x25519": tls.X25519,
139+
"X25519MLKEM768": tls.X25519MLKEM768,
140+
}
141+
142+
func (c *Config) parseNamedGroups(input *TLSDetailsFile) (bool, error) {
143+
if input.NamedGroups == nil {
144+
return false, nil
145+
}
146+
ids := []tls.CurveID{}
147+
for _, name := range input.NamedGroups {
148+
id, ok := groupsByName[name]
149+
if !ok {
150+
return false, fmt.Errorf("unrecognized TLS named group %q", name)
151+
}
152+
ids = append(ids, id)
153+
}
154+
c.text.NamedGroups = slices.Clone(input.NamedGroups)
155+
c.config.CurvePreferences = ids
156+
return true, nil
157+
}
158+
159+
// TLSConfig returns a *tls.Config matching the provided settings.
160+
// If c contains no settings, it returns nil.
161+
// Otherwise, the returned *tls.Config is freshly allocated and the caller can modify it as needed.
162+
func (c *Config) TLSConfig() *tls.Config {
163+
if c.config == nil {
164+
return nil
165+
}
166+
return c.config.Clone()
167+
}
168+
169+
// marshaledSerialization is the data we use in MarshalText/UnmarshalText,
170+
// marshaled using JSON.
171+
//
172+
// Note that the file format is using YAML, but we use JSON, to minimize dependencies
173+
// in backend code where we don't need comments and the brackets are not annoying users.
174+
type marshaledSerialization struct {
175+
Version int
176+
Data TLSDetailsFile
177+
}
178+
179+
const marshaledSerializationVersion1 = 1
180+
181+
// MarshalText serializes c to a text representation.
182+
//
183+
// The representation is intended to be reasonably stable across updates to c/image,
184+
// but the consumer must not be older than the producer.
185+
func (c Config) MarshalText() ([]byte, error) {
186+
data := marshaledSerialization{
187+
Version: marshaledSerializationVersion1,
188+
Data: c.text,
189+
}
190+
return json.Marshal(data)
191+
}
192+
193+
// UnmarshalText parses the output of MarshalText.
194+
//
195+
// The format is otherwise undocumented and we do not promise ongoing compatibility with producers external to this package.
196+
func (c *Config) UnmarshalText(text []byte) error {
197+
var data marshaledSerialization
198+
199+
// In the future, this should be an even stricter parser, e.g. refusing duplicate fields
200+
// and requiring a case-sensitive field name match.
201+
decoder := json.NewDecoder(bytes.NewReader(text))
202+
decoder.DisallowUnknownFields()
203+
if err := decoder.Decode(&data); err != nil {
204+
return err
205+
}
206+
if decoder.More() {
207+
return errors.New("unexpected extra data after a JSON object")
208+
}
209+
210+
if data.Version != marshaledSerializationVersion1 {
211+
return fmt.Errorf("unsupported version %d", data.Version)
212+
}
213+
v, err := NewFromTLSDetails(&data.Data)
214+
if err != nil {
215+
return err
216+
}
217+
*c = *v
218+
return nil
219+
}

0 commit comments

Comments
 (0)