Skip to content

Commit 74c1254

Browse files
committed
Add generation and registration of the AK
Generate and persistent the AK in the TPM if the key registration is request in the config. The public AK is then registered at the url specified in the config. The AK is generate only once and if the /var/tpm/ak.pub exists then it means that this step has already been performed and it is skipped of the next stages. The registration of the AK required networking hence, during the fetch-offline stage it signals that the networking is necessary. The retries mechanism ensures that the registration is tried multiple times for allowing the parallel network configuration in the fetch phase. Signed-off-by: Alice Frosi <afrosi@redhat.com>
1 parent 3369334 commit 74c1254

File tree

2 files changed

+250
-4
lines changed

2 files changed

+250
-4
lines changed
Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
// Copyright 2025 CoreOS, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package attestation
16+
17+
import (
18+
"bytes"
19+
"crypto/tls"
20+
"crypto/x509"
21+
"encoding/json"
22+
"encoding/pem"
23+
"errors"
24+
"fmt"
25+
"net"
26+
"net/http"
27+
"os"
28+
"os/exec"
29+
"syscall"
30+
"time"
31+
32+
"github.com/coreos/ignition/v2/config/util"
33+
"github.com/coreos/ignition/v2/config/v3_6_experimental/types"
34+
"github.com/coreos/ignition/v2/internal/log"
35+
"github.com/coreos/ignition/v2/internal/resource"
36+
)
37+
38+
const (
39+
TPMDir = "/var/tpm"
40+
AKPath = "/var/tpm/ak.pub"
41+
AKCtxPath = "/var/tpm/ak.ctx"
42+
AKRegisterd = "/var/tpm/ak.registerd"
43+
AKHandle = "0x81010002"
44+
EKHandle = "0x81010001"
45+
)
46+
47+
func HandleAttestation(logger *log.Logger, cfg *types.Config, platformName string, needNetPath string) error {
48+
if !util.NilOrEmpty(cfg.Attestation.AttestationKey.Registration.Url) {
49+
// Generate and persist the AK
50+
if err := GenerateAndPersistAK(logger); err != nil {
51+
return err
52+
}
53+
54+
attestationKeyBytes, err := os.ReadFile(AKPath)
55+
if err != nil {
56+
return err
57+
}
58+
attestationKey := string(attestationKeyBytes)
59+
60+
// Check if the neednet file exists to determine our retry behavior
61+
_, needNetErr := os.Stat(needNetPath)
62+
needNetExists := (needNetErr == nil)
63+
if needNetExists {
64+
logger.Info("neednet file exists, network should be available for attestation")
65+
} else {
66+
logger.Info("neednet file does not exist, will return ErrNeedNet if network is unavailable")
67+
}
68+
69+
err = AttestationKeyRegistration(logger, cfg.Attestation.AttestationKey.Registration,
70+
attestationKey, platformName)
71+
if err != nil {
72+
// If we got ErrNeedNet and the neednet file doesn't exist, propagate it
73+
// (we're in fetch-offline and need to signal for network)
74+
if err == resource.ErrNeedNet && !needNetExists {
75+
return err
76+
}
77+
// If we got ErrNeedNet but neednet file exists, we're in fetch stage
78+
// Retry the registration with delays to allow network to come up
79+
if err == resource.ErrNeedNet && needNetExists {
80+
logger.Info("Network not ready yet in fetch stage, retrying with delays...")
81+
// Retry up to 10 times with increasing delays
82+
maxRetries := 20
83+
for attempt := 2; attempt <= maxRetries; attempt++ {
84+
delay := time.Duration(min(attempt*2, 10)) * time.Second
85+
logger.Info("Waiting %v before retry attempt %d/%d", delay, attempt, maxRetries)
86+
time.Sleep(delay)
87+
88+
err = AttestationKeyRegistration(logger, cfg.Attestation.AttestationKey.Registration,
89+
attestationKey, platformName)
90+
if err == nil {
91+
break
92+
}
93+
logger.Info("Attestation registration attempt %d/%d failed: %v", attempt, maxRetries, err)
94+
}
95+
if err != nil {
96+
return fmt.Errorf("failed to register attestation key after retries: %w", err)
97+
}
98+
} else {
99+
return err
100+
}
101+
}
102+
}
103+
return nil
104+
}
105+
106+
// GenerateAndPersistAK creates and persists the Attestation Key in the TPM
107+
func GenerateAndPersistAK(logger *log.Logger) error {
108+
if err := os.MkdirAll(TPMDir, 0755); err != nil {
109+
return fmt.Errorf("couldn't create %s directory: %w", TPMDir, err)
110+
}
111+
112+
if _, err := os.Stat(AKPath); err == nil {
113+
logger.Info("Attestation Key already exists, skipping generation")
114+
return nil
115+
}
116+
117+
logger.Info("Generating Attestation Key")
118+
cmd := exec.Command("tpm2_createak", "-C", EKHandle,
119+
"-c", AKCtxPath, "-G", "rsa", "-g", "sha256",
120+
"-s", "rsassa", "-u", AKPath, "-f", "pem")
121+
if _, err := logger.LogCmd(cmd, "creating attestation key"); err != nil {
122+
return fmt.Errorf("failed to create attestation key: %w", err)
123+
}
124+
125+
cmd = exec.Command("tpm2_evictcontrol", "-c", AKCtxPath, AKHandle)
126+
if _, err := logger.LogCmd(cmd, "persisting attestation key"); err != nil {
127+
return fmt.Errorf("failed to persist attestation key: %w", err)
128+
}
129+
130+
return nil
131+
}
132+
133+
// AttestationKeyRegistration sends a request to register an attestation key
134+
func AttestationKeyRegistration(logger *log.Logger, registration types.Registration, attestationKey string, platform string) error {
135+
if registration.Url == nil || *registration.Url == "" {
136+
return fmt.Errorf("registration URL is required")
137+
}
138+
// Check if AK was already generated
139+
if _, err := os.Stat(AKRegisterd); err == nil {
140+
return nil
141+
}
142+
143+
requestBody := map[string]string{
144+
"attestation_key": attestationKey,
145+
"platform": platform,
146+
}
147+
148+
jsonBody, err := json.Marshal(requestBody)
149+
if err != nil {
150+
return fmt.Errorf("failed to marshal request body: %w", err)
151+
}
152+
153+
client := &http.Client{}
154+
155+
if !util.NilOrEmpty(registration.Certificate) {
156+
tlsConfig, err := createTLSConfig(*registration.Certificate)
157+
if err != nil {
158+
return fmt.Errorf("failed to create TLS config: %w", err)
159+
}
160+
161+
client.Transport = &http.Transport{
162+
TLSClientConfig: tlsConfig,
163+
}
164+
}
165+
166+
// Single attempt - caller (HandleAttestation) handles retries
167+
req, err := http.NewRequest(http.MethodPut, *registration.Url, bytes.NewBuffer(jsonBody))
168+
if err != nil {
169+
return fmt.Errorf("failed to create request: %w", err)
170+
}
171+
172+
req.Header.Set("Content-Type", "application/json")
173+
174+
resp, err := client.Do(req)
175+
if err != nil {
176+
// Return network errors as ErrNeedNet for caller to handle
177+
if isNetworkUnreachable(err) {
178+
return resource.ErrNeedNet
179+
}
180+
return fmt.Errorf("failed to register attestation key: %w", err)
181+
}
182+
183+
defer resp.Body.Close()
184+
185+
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
186+
return fmt.Errorf("registration failed with status code: %d", resp.StatusCode)
187+
}
188+
189+
// Registration successful
190+
if err := os.WriteFile(AKRegisterd, []byte{}, 0644); err != nil {
191+
return fmt.Errorf("failed to create AK registered file: %w", err)
192+
}
193+
logger.Info("Register successfully the AK")
194+
return nil
195+
}
196+
197+
// isNetworkUnreachable checks if the error indicates network is unreachable
198+
func isNetworkUnreachable(err error) bool {
199+
var opErr *net.OpError
200+
if errors.As(err, &opErr) {
201+
// Check for ENETUNREACH (network unreachable)
202+
if errors.Is(opErr.Err, syscall.ENETUNREACH) {
203+
return true
204+
}
205+
// Check for EHOSTUNREACH (host unreachable)
206+
if errors.Is(opErr.Err, syscall.EHOSTUNREACH) {
207+
return true
208+
}
209+
// Check for "connect: network is unreachable" string
210+
if opErr.Err != nil && opErr.Err.Error() == "network is unreachable" {
211+
return true
212+
}
213+
}
214+
return false
215+
}
216+
217+
func createTLSConfig(certPEM string) (*tls.Config, error) {
218+
block, _ := pem.Decode([]byte(certPEM))
219+
if block == nil {
220+
return nil, fmt.Errorf("failed to decode PEM certificate")
221+
}
222+
223+
cert, err := x509.ParseCertificate(block.Bytes)
224+
if err != nil {
225+
return nil, fmt.Errorf("failed to parse certificate: %w", err)
226+
}
227+
228+
certPool := x509.NewCertPool()
229+
certPool.AddCert(cert)
230+
231+
return &tls.Config{
232+
RootCAs: certPool,
233+
}, nil
234+
}

internal/exec/engine.go

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626
"github.com/coreos/ignition/v2/config/shared/errors"
2727
latest "github.com/coreos/ignition/v2/config/v3_6_experimental"
2828
"github.com/coreos/ignition/v2/config/v3_6_experimental/types"
29+
"github.com/coreos/ignition/v2/internal/attestation"
2930
"github.com/coreos/ignition/v2/internal/exec/stages"
3031
executil "github.com/coreos/ignition/v2/internal/exec/util"
3132
"github.com/coreos/ignition/v2/internal/log"
@@ -176,7 +177,7 @@ func logStructuredJournalEntry(cfgInfo state.FetchedConfig) error {
176177
func (e *Engine) acquireConfig(stageName string) (cfg types.Config, err error) {
177178
switch {
178179
case strings.HasPrefix(stageName, "fetch"):
179-
cfg, err = e.acquireProviderConfig()
180+
cfg, err = e.acquireProviderConfig(stageName)
180181

181182
// if we've successfully fetched and cached the configs, log about them
182183
if err == nil && journal.Enabled() {
@@ -216,7 +217,7 @@ func (e *Engine) acquireCachedConfig() (cfg types.Config, err error) {
216217

217218
// acquireProviderConfig attempts to fetch the configuration from the
218219
// provider.
219-
func (e *Engine) acquireProviderConfig() (cfg types.Config, err error) {
220+
func (e *Engine) acquireProviderConfig(stageName string) (cfg types.Config, err error) {
220221
// Create a new http client and fetcher with the timeouts set via the flags,
221222
// since we don't have a config with timeout values we can use
222223
timeout := int(e.FetchTimeout.Seconds())
@@ -228,7 +229,7 @@ func (e *Engine) acquireProviderConfig() (cfg types.Config, err error) {
228229
}
229230

230231
// (Re)Fetch the config if the cache is unreadable.
231-
cfg, err = e.fetchProviderConfig()
232+
cfg, err = e.fetchProviderConfig(stageName)
232233
if err == errors.ErrEmpty {
233234
// Continue if the provider config was empty as we want to write an empty
234235
// cache config for use by other stages.
@@ -285,7 +286,7 @@ func (e *Engine) acquireProviderConfig() (cfg types.Config, err error) {
285286
// it checks the config engine's provider. An error is returned if the provider
286287
// is unavailable. This will also render the config (see renderConfig) before
287288
// returning.
288-
func (e *Engine) fetchProviderConfig() (types.Config, error) {
289+
func (e *Engine) fetchProviderConfig(stageName string) (types.Config, error) {
289290
platformConfigs := []platform.Config{
290291
cmdline.Config,
291292
system.Config,
@@ -315,6 +316,17 @@ func (e *Engine) fetchProviderConfig() (types.Config, error) {
315316
Referenced: false,
316317
})
317318

319+
if err := attestation.HandleAttestation(e.Logger, &cfg, e.PlatformConfig.Name(), e.NeedNet); err != nil {
320+
if err == resource.ErrNeedNet && stageName == "fetch-offline" {
321+
err = e.signalNeedNet()
322+
if err != nil {
323+
e.Logger.Crit("failed to signal neednet: %v", err)
324+
}
325+
return cfg, resource.ErrNeedNet
326+
}
327+
return types.Config{}, err
328+
}
329+
318330
// Replace the HTTP client in the fetcher to be configured with the
319331
// timeouts of the config
320332
err = e.Fetcher.UpdateHttpTimeoutsAndCAs(cfg.Ignition.Timeouts, cfg.Ignition.Security.TLS.CertificateAuthorities, cfg.Ignition.Proxy)

0 commit comments

Comments
 (0)