Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 38 additions & 5 deletions internal/certs/lmsTls.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
package certs

import (
"bytes"
"crypto/sha256"
"crypto/tls"
"crypto/x509"
Expand All @@ -18,11 +19,14 @@ import (

// generates a TLS configuration based on the provided mode.
func GetTLSConfig(mode *int, amtCertInfo *amt.SecureHBasedResponse, skipAMTCertCheck bool) *tls.Config {
tlsConfig := &tls.Config{}

tlsConfig.InsecureSkipVerify = skipAMTCertCheck
tlsConfig := &tls.Config{
InsecureSkipVerify: skipAMTCertCheck,
}

if *mode == 0 { // pre-provisioning mode
// Pre-provisioning uses AMT loopback/self-signed TLS; allow handshake and
// enforce certificate validation in VerifyPeerCertificate.
tlsConfig.InsecureSkipVerify = true
tlsConfig.VerifyPeerCertificate = func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
if skipAMTCertCheck {
return nil
Expand All @@ -32,7 +36,11 @@ func GetTLSConfig(mode *int, amtCertInfo *amt.SecureHBasedResponse, skipAMTCertC
}
} else {
// default tls config if device is in ACM or CCM
log.Trace("Setting default TLS Config for ACM/CCM mode")
if skipAMTCertCheck {
log.Trace("Skipping AMT certificate verification for ACM/CCM mode (loopback TLS)")
} else {
Comment on lines 37 to +41
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In GetTLSConfig(), the ACM/CCM branch leaves default TLS verification enabled (InsecureSkipVerify stays false when skipAMTCertCheck is false) and does not set VerifyPeerCertificate. For AMT 19+ loopback TLS where LMS presents a self-signed/untrusted certificate, this will still fail the handshake. Consider always enabling InsecureSkipVerify for loopback and performing certificate validation via VerifyPeerCertificate/VerifyCertificates for all modes when skipAMTCertCheck is false.

Copilot uses AI. Check for mistakes.
log.Trace("Using default TLS config for ACM/CCM mode")
}
}

return tlsConfig
Expand Down Expand Up @@ -62,7 +70,7 @@ func VerifyCertificates(rawCerts [][]byte, mode *int, amtCertInfo *amt.SecureHBa
return err
}

log.Infof("Cert[%d]: Subject=%s, Issuer=%s, EKU=%v", i, cert.Subject, cert.Issuer, cert.ExtKeyUsage)
log.Tracef("Cert[%d]: Subject=%s, Issuer=%s, EKU=%v", i, cert.Subject, cert.Issuer, cert.ExtKeyUsage)

parsedCerts = append(parsedCerts, cert)

Expand All @@ -84,6 +92,31 @@ func VerifyCertificates(rawCerts [][]byte, mode *int, amtCertInfo *amt.SecureHBa

return nil
case selfSignedChainLength:
// On AMT 19+ loopback TLS, the LMS/AMT certificate is typically a single
// self-signed certificate that is not rooted in the system trust store.
// In pre-provisioning mode (mode == 0), accept this only when the leaf
// certificate matches AMT loopback expectations.
if mode != nil && *mode == 0 {
cert, err := x509.ParseCertificate(rawCerts[0])
if err != nil {
log.Error("Failed to parse self-signed AMT loopback certificate:", err)

return err
}

if !bytes.Equal(cert.RawSubject, cert.RawIssuer) {
return errors.New("single AMT loopback certificate is not self-signed")
}
Comment on lines +106 to +109
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The self-signed check compares cert.RawSubject and cert.RawIssuer, which is not sufficient to prove the certificate is actually self-signed (same DN can still be signed by another key). Use a signature check (e.g., cert.CheckSignatureFrom(cert)) before treating it as self-signed.

Copilot uses AI. Check for mistakes.

if err := VerifyLeafCertificate(cert, amtCertInfo); err != nil {
return err
}

log.Trace("Accepting self-signed AMT loopback certificate in pre-provisioning mode")

return nil
}

return HandleAMTTransition(mode)
}

Expand Down
25 changes: 25 additions & 0 deletions internal/certs/lmsTls_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,11 @@ func TestGetTLSConfig(t *testing.T) {
assert.True(t, tlsConfig.InsecureSkipVerify)
assert.NotNil(t, tlsConfig.VerifyPeerCertificate)

tlsConfig = GetTLSConfig(&mode, nil, false)
assert.NotNil(t, tlsConfig)
assert.True(t, tlsConfig.InsecureSkipVerify)
assert.NotNil(t, tlsConfig.VerifyPeerCertificate)

mode = 1
tlsConfig = GetTLSConfig(&mode, nil, true)
assert.NotNil(t, tlsConfig)
Expand Down Expand Up @@ -299,3 +304,23 @@ func TestVerifyFullChain(t *testing.T) {
})
}
}

func TestVerifyCertificates_SingleCertPreProvisioning(t *testing.T) {
t.Run("accepts allowed self-signed leaf CN", func(t *testing.T) {
mode := 0
leafTemplate := createCertTemplate("AMT RCFG", false, []string{"Leaf OU"})
leafCert, _ := createTestCert(t, leafTemplate, nil, nil)

err := VerifyCertificates([][]byte{leafCert.Raw}, &mode, nil)
assert.NoError(t, err)
})

t.Run("rejects invalid self-signed leaf CN", func(t *testing.T) {
mode := 0
leafTemplate := createCertTemplate("Invalid CN", false, []string{"Leaf OU"})
leafCert, _ := createTestCert(t, leafTemplate, nil, nil)

err := VerifyCertificates([][]byte{leafCert.Raw}, &mode, nil)
assert.Error(t, err)
})
}
1 change: 1 addition & 0 deletions internal/cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,7 @@ func ExecuteWithAMT(args []string, amtCommand amt.Interface) error {

appCtx := &commands.Context{
AMTCommand: amtCommand,
LocalTLSEnforced: false,
LogLevel: cli.LogLevel,
JsonOutput: cli.JsonOutput,
TableOutput: cli.TableOutput,
Expand Down
3 changes: 3 additions & 0 deletions internal/commands/activate/activate.go
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,9 @@ func (cmd *ActivateCmd) Run(ctx *commands.Context) error {

// runRemoteActivation executes remote activation using the remote service
func (cmd *ActivateCmd) runRemoteActivation(ctx *commands.Context) error {
// Propagate local TLS enforcement status detected in AMTBaseCmd.AfterApply
ctx.LocalTLSEnforced = cmd.LocalTLSEnforced
Comment on lines +261 to +262
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

runRemoteActivation propagates LocalTLSEnforced into the shared Context, but Context.ControlMode is still never set and RemoteActivateCmd later forwards it into the RPS request. Propagate the detected control mode as well (e.g., ctx.ControlMode = cmd.GetControlMode()) so RPS/LMS TLS config selection is based on the real AMT mode.

Suggested change
// Propagate local TLS enforcement status detected in AMTBaseCmd.AfterApply
ctx.LocalTLSEnforced = cmd.LocalTLSEnforced
// Propagate activation state detected in AMTBaseCmd.AfterApply
ctx.LocalTLSEnforced = cmd.LocalTLSEnforced
ctx.ControlMode = cmd.GetControlMode()

Copilot uses AI. Check for mistakes.

// Create remote activation command with current flags
remoteCmd := RemoteActivateCmd{
URL: cmd.URL,
Expand Down
1 change: 1 addition & 0 deletions internal/commands/activate/remote.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ func (service *RemoteActivationService) requestActivation(deviceInfo map[string]
Verbose: service.context.Verbose,
SkipCertCheck: service.context.SkipCertCheck,
SkipAmtCertCheck: service.context.SkipAMTCertCheck,
LocalTLSEnforced: service.context.LocalTLSEnforced,
ControlMode: service.context.ControlMode,
Comment on lines 169 to 173
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ControlMode is taken from service.context.ControlMode here, but Context.ControlMode does not appear to be populated anywhere (AMTBaseCmd stores control mode on the command, not the Context). This likely means RPS always receives ControlMode=0; propagate the detected mode into the Context before invoking RemoteActivateCmd, or set ControlMode explicitly when building this request.

Copilot uses AI. Check for mistakes.
TenantID: service.context.TenantID,
Password: service.context.AMTPassword,
Expand Down
1 change: 1 addition & 0 deletions internal/commands/deactivate.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ func (cmd *DeactivateCmd) executeRemoteDeactivate(ctx *Context) error {
Verbose: ctx.Verbose,
SkipCertCheck: ctx.SkipCertCheck,
SkipAmtCertCheck: ctx.SkipAMTCertCheck,
LocalTLSEnforced: cmd.LocalTLSEnforced,
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This RPS request does not populate ControlMode. Since RPS executor uses ControlMode to build the LMS TLS config, leaving it as the zero value can cause incorrect TLS behavior when local TLS is enforced. Set ControlMode from the detected AMT mode (e.g., cmd.GetControlMode() / cmd.ControlMode).

Suggested change
LocalTLSEnforced: cmd.LocalTLSEnforced,
LocalTLSEnforced: cmd.LocalTLSEnforced,
ControlMode: cmd.GetControlMode(),

Copilot uses AI. Check for mistakes.
Force: cmd.Force,
TenantID: ctx.TenantID,
}
Expand Down
25 changes: 19 additions & 6 deletions internal/commands/deactivate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -574,27 +574,40 @@ func TestDeactivateCmd_ResolveGUID(t *testing.T) {
// Test setupTLSConfig function
func TestSetupTLSConfig(t *testing.T) {
t.Run("TLS config with LocalTLSEnforced false", func(t *testing.T) {
cmd := &DeactivateCmd{}
cmd.LocalTLSEnforced = true
ctx := &Context{ControlMode: ControlModeACM}
cmd := &DeactivateCmd{
AMTBaseCmd: AMTBaseCmd{
ControlMode: ControlModeACM,
},
}
cmd.LocalTLSEnforced = false
ctx := &Context{
ControlMode: ControlModeACM,
SkipAMTCertCheck: true, // Should be ignored when not enforced
}

tlsConfig := cmd.setupTLSConfig(ctx)

assert.NotNil(t, tlsConfig)
// When TLS is not enforced locally, we expect default config which has InsecureSkipVerify false
assert.False(t, tlsConfig.InsecureSkipVerify)
})

t.Run("TLS config with LocalTLSEnforced true", func(t *testing.T) {
cmd := &DeactivateCmd{}
cmd := &DeactivateCmd{
AMTBaseCmd: AMTBaseCmd{
ControlMode: ControlModeACM,
},
}
cmd.LocalTLSEnforced = true
ctx := &Context{
SkipCertCheck: true,
SkipCertCheck: true, // Should be ignored by setupTLSConfig
ControlMode: ControlModeACM,
}

tlsConfig := cmd.setupTLSConfig(ctx)

assert.NotNil(t, tlsConfig)
// The actual config setup depends on the config.GetTLSConfig implementation
// When LocalTLSEnforced is true, we use SkipAMTCertCheck (which is false here)
assert.False(t, tlsConfig.InsecureSkipVerify)
})
}
17 changes: 9 additions & 8 deletions internal/commands/shared.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,15 @@ import (

// Context holds shared dependencies injected into commands
type Context struct {
AMTCommand amt.Interface
ControlMode int
LogLevel string
JsonOutput bool
TableOutput bool
NoColor bool
Verbose bool
SkipCertCheck bool
AMTCommand amt.Interface
ControlMode int
LocalTLSEnforced bool
LogLevel string
JsonOutput bool
TableOutput bool
NoColor bool
Verbose bool
SkipCertCheck bool
// SkipAMTCertCheck controls whether to skip TLS verification when connecting to AMT/LMS over TLS
// This is distinct from SkipCertCheck which applies to remote RPS HTTPS/WSS connections.
SkipAMTCertCheck bool
Expand Down
8 changes: 4 additions & 4 deletions internal/rps/executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ type Executor struct {
type ExecutorConfig struct {
URL string
Proxy string
LocalTlsEnforced bool
LocalTLSEnforced bool
SkipAmtCertCheck bool
ControlMode int
SkipCertCheck bool
Expand All @@ -46,13 +46,13 @@ func NewExecutor(config ExecutorConfig) (Executor, error) {
lmErrorChannel := make(chan error)

port := utils.LMSPort
if config.LocalTlsEnforced {
if config.LocalTLSEnforced {
port = utils.LMSTLSPort
}

client := Executor{
server: NewAMTActivationServer(config.URL, config.Proxy),
localManagement: lm.NewLMSConnection(utils.LMSAddress, port, config.LocalTlsEnforced, lmDataChannel, lmErrorChannel, config.ControlMode, config.SkipAmtCertCheck),
localManagement: lm.NewLMSConnection(utils.LMSAddress, port, config.LocalTLSEnforced, lmDataChannel, lmErrorChannel, config.ControlMode, config.SkipAmtCertCheck),
data: lmDataChannel,
errors: lmErrorChannel,
waitGroup: &sync.WaitGroup{},
Expand All @@ -61,7 +61,7 @@ func NewExecutor(config ExecutorConfig) (Executor, error) {
// TEST CONNECTION TO SEE IF LMS EXISTS
err := client.localManagement.Connect()
if err != nil {
if config.LocalTlsEnforced {
if config.LocalTLSEnforced {
return client, utils.LMSConnectionFailed
}
// client.localManagement.Close()
Expand Down
2 changes: 2 additions & 0 deletions internal/rps/message.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ type MessagePayload struct {
CertificateHashes []string `json:"certHashes"`
IPConfiguration IPConfiguration `json:"ipConfiguration"`
HostnameInfo HostnameInfo `json:"hostnameInfo"`
LocalTLSEnforced bool `json:"localTlsEnforced,omitempty"`
FriendlyName string `json:"friendlyName,omitempty"`
}

Expand Down Expand Up @@ -197,6 +198,7 @@ func (p Payload) CreateMessageRequest(req Request) (Message, error) {

payload.IPConfiguration = req.IpConfiguration
payload.HostnameInfo = req.HostnameInfo
payload.LocalTLSEnforced = req.LocalTLSEnforced

if req.UUID != "" {
if isKnownInvalidUUID(req.UUID) {
Expand Down
37 changes: 37 additions & 0 deletions internal/rps/message_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -437,3 +437,40 @@ func TestCreateMessageRequestWithInvalidUUIDPattern(t *testing.T) {
assert.Error(t, createErr)
assert.Equal(t, utils.InvalidUUID, createErr)
}

func TestCreateMessageRequestLocalTLSEnforced(t *testing.T) {
tests := []struct {
name string
localTLSEnforced bool
expectEnforced bool
}{
{
name: "true",
localTLSEnforced: true,
expectEnforced: true,
},
{
name: "false",
localTLSEnforced: false,
expectEnforced: false,
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
flags := Request{
LocalTLSEnforced: tc.localTLSEnforced,
}
result, createErr := p.CreateMessageRequest(flags)
assert.NoError(t, createErr)
assert.NotEmpty(t, result.Payload)
decodedBytes, decodeErr := base64.StdEncoding.DecodeString(result.Payload)
assert.NoError(t, decodeErr)

msgPayload := MessagePayload{}
jsonErr := json.Unmarshal(decodedBytes, &msgPayload)
assert.NoError(t, jsonErr)
assert.Equal(t, tc.expectEnforced, msgPayload.LocalTLSEnforced)
})
}
}
2 changes: 1 addition & 1 deletion internal/rps/rps.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ func ExecuteCommand(req *Request) error {
config := ExecutorConfig{
URL: req.URL,
Proxy: req.Proxy,
LocalTlsEnforced: req.LocalTlsEnforced,
LocalTLSEnforced: req.LocalTLSEnforced,
SkipAmtCertCheck: req.SkipAmtCertCheck,
ControlMode: req.ControlMode,
SkipCertCheck: req.SkipCertCheck,
Expand Down
2 changes: 1 addition & 1 deletion internal/rps/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ type Request struct {
// Connection and server parameters
URL string
Proxy string
LocalTlsEnforced bool
LocalTLSEnforced bool
SkipAmtCertCheck bool
ControlMode int
SkipCertCheck bool
Expand Down
Loading