Skip to content

Commit 85608d6

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 470a43f commit 85608d6

File tree

2 files changed

+260
-4
lines changed

2 files changed

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

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)