From fa4e57ff167c821a9a32235c1d03ae9e25ac53a0 Mon Sep 17 00:00:00 2001 From: Tim Shockley Date: Wed, 6 Sep 2023 14:56:43 -0700 Subject: [PATCH] feat: amtinfo display user certificates --- internal/flags/info.go | 28 ++++++++--- internal/flags/info_test.go | 92 +++++++++++++++++++++++++++++++++ internal/local/info.go | 98 +++++++++++++++++++++++++++--------- internal/local/info_test.go | 55 +++++++++++++++----- internal/local/lps_test.go | 21 ++++++++ internal/local/utils.go | 11 ++++ internal/local/utils_test.go | 16 ++++-- 7 files changed, 274 insertions(+), 47 deletions(-) create mode 100644 internal/flags/info_test.go diff --git a/internal/flags/info.go b/internal/flags/info.go index 37fceba4..45af0b0b 100644 --- a/internal/flags/info.go +++ b/internal/flags/info.go @@ -13,31 +13,33 @@ type AmtInfoFlags struct { Mode bool DNS bool Cert bool + UserCert bool Ras bool Lan bool Hostname bool } -// TODO: write unit tests func (f *Flags) handleAMTInfo(amtInfoCommand *flag.FlagSet) int { + // runs locally + f.Local = true + amtInfoCommand.BoolVar(&f.AmtInfo.Ver, "ver", false, "BIOS Version") amtInfoCommand.BoolVar(&f.AmtInfo.Bld, "bld", false, "Build Number") amtInfoCommand.BoolVar(&f.AmtInfo.Sku, "sku", false, "Product SKU") amtInfoCommand.BoolVar(&f.AmtInfo.UUID, "uuid", false, "Unique Identifier") amtInfoCommand.BoolVar(&f.AmtInfo.Mode, "mode", false, "Current Control Mode") amtInfoCommand.BoolVar(&f.AmtInfo.DNS, "dns", false, "Domain Name Suffix") - amtInfoCommand.BoolVar(&f.AmtInfo.Cert, "cert", false, "Certificate Hashes") + amtInfoCommand.BoolVar(&f.AmtInfo.Cert, "cert", false, "System Certificate Hashes and User Certificates if AMT password is provided") + amtInfoCommand.BoolVar(&f.AmtInfo.UserCert, "userCert", false, "User Certificates only. AMT password is required") amtInfoCommand.BoolVar(&f.AmtInfo.Ras, "ras", false, "Remote Access Status") amtInfoCommand.BoolVar(&f.AmtInfo.Lan, "lan", false, "LAN Settings") amtInfoCommand.BoolVar(&f.AmtInfo.Hostname, "hostname", false, "OS Hostname") + amtInfoCommand.StringVar(&f.Password, "password", f.lookupEnvOrString("AMT_PASSWORD", ""), "AMT Password") if err := amtInfoCommand.Parse(f.commandLineArgs[2:]); err != nil { return utils.IncorrectCommandLineParameters } - // runs locally - f.Local = true - defaultFlagCount := 2 if f.JsonOutput { defaultFlagCount = defaultFlagCount + 1 @@ -49,10 +51,24 @@ func (f *Flags) handleAMTInfo(amtInfoCommand *flag.FlagSet) int { f.AmtInfo.UUID = true f.AmtInfo.Mode = true f.AmtInfo.DNS = true - f.AmtInfo.Cert = false f.AmtInfo.Ras = true f.AmtInfo.Lan = true f.AmtInfo.Hostname = true } + + // no password - same behavior only cert hashes + // with password - shows user certs too + // (don't break previous behavior) + if f.AmtInfo.Cert && f.Password != "" { + f.AmtInfo.UserCert = true + } + + // user certs need the amt password for the local wsman connection + if f.AmtInfo.UserCert && f.Password == "" { + if _, errCode := f.ReadPasswordFromUser(); errCode != 0 { + return utils.MissingOrIncorrectPassword + } + } + return utils.Success } diff --git a/internal/flags/info_test.go b/internal/flags/info_test.go new file mode 100644 index 00000000..6a673e03 --- /dev/null +++ b/internal/flags/info_test.go @@ -0,0 +1,92 @@ +package flags + +import ( + "github.com/stretchr/testify/assert" + "rpc/pkg/utils" + "strings" + "testing" +) + +func TestParseFlagsAmtInfo(t *testing.T) { + defaultFlags := AmtInfoFlags{ + Ver: true, + Bld: true, + Sku: true, + UUID: true, + Mode: true, + DNS: true, + Ras: true, + Lan: true, + Hostname: true, + } + + tests := map[string]struct { + cmdLine string + wantResult int + wantFlags AmtInfoFlags + userInput string + }{ + "expect success for basic command": { + cmdLine: "./rpc amtinfo -json", + wantResult: utils.Success, + wantFlags: defaultFlags, + }, + "expect IncorrectCommandLineParameters on Parse error": { + cmdLine: "./rpc amtinfo -balderdash", + wantResult: utils.IncorrectCommandLineParameters, + wantFlags: AmtInfoFlags{}, + }, + "expect only cert flag with no password on command line": { + cmdLine: "./rpc amtinfo -cert", + wantResult: utils.Success, + wantFlags: AmtInfoFlags{ + Cert: true, + }, + }, + "expect both cert flags with no password on command line": { + cmdLine: "./rpc amtinfo -cert -password testPassword", + wantResult: utils.Success, + wantFlags: AmtInfoFlags{ + Cert: true, + UserCert: true, + }, + }, + "expect MissingOrIncorrectPassword for userCert": { + cmdLine: "./rpc amtinfo -userCert", + wantResult: utils.MissingOrIncorrectPassword, + wantFlags: AmtInfoFlags{ + UserCert: true, + }, + }, + "expect Success for userCert with password": { + cmdLine: "./rpc amtinfo -userCert -password testPassword", + wantResult: utils.Success, + wantFlags: AmtInfoFlags{ + UserCert: true, + }, + }, + "expect Success for userCert with password input": { + cmdLine: "./rpc amtinfo -userCert", + wantResult: utils.Success, + wantFlags: AmtInfoFlags{ + UserCert: true, + }, + userInput: "testPassword", + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + args := strings.Fields(tc.cmdLine) + if tc.userInput != "" { + defer userInput(t, tc.userInput)() + } + flags := NewFlags(args) + gotResult := flags.ParseFlags() + assert.Equal(t, tc.wantResult, gotResult) + assert.Equal(t, true, flags.Local) + assert.Equal(t, utils.CommandAMTInfo, flags.Command) + assert.Equal(t, tc.wantFlags, flags.AmtInfo) + }) + } +} diff --git a/internal/local/info.go b/internal/local/info.go index ab4ff57d..9a397f8f 100644 --- a/internal/local/info.go +++ b/internal/local/info.go @@ -3,6 +3,8 @@ package local import ( "encoding/json" "fmt" + "github.com/open-amt-cloud-toolkit/go-wsman-messages/pkg/amt/publickey" + "github.com/open-amt-cloud-toolkit/go-wsman-messages/pkg/amt/publicprivate" log "github.com/sirupsen/logrus" "os" "rpc/internal/amt" @@ -11,12 +13,16 @@ import ( "strings" ) +type PrivateKeyPairReference struct { + KeyPair publicprivate.KeyPair + AssociatedCerts []string +} + func (service *ProvisioningService) DisplayAMTInfo() int { dataStruct := make(map[string]interface{}) - - amtCommand := amt.NewAMTCommand() + cmd := service.amtCommand if service.flags.AmtInfo.Ver { - result, err := amtCommand.GetVersionDataFromME("AMT", service.flags.AMTTimeoutDuration) + result, err := cmd.GetVersionDataFromME("AMT", service.flags.AMTTimeoutDuration) if err != nil { log.Error(err) } @@ -26,7 +32,7 @@ func (service *ProvisioningService) DisplayAMTInfo() int { } } if service.flags.AmtInfo.Bld { - result, err := amtCommand.GetVersionDataFromME("Build Number", service.flags.AMTTimeoutDuration) + result, err := cmd.GetVersionDataFromME("Build Number", service.flags.AMTTimeoutDuration) if err != nil { log.Error(err) } @@ -37,7 +43,7 @@ func (service *ProvisioningService) DisplayAMTInfo() int { } } if service.flags.AmtInfo.Sku { - result, err := amtCommand.GetVersionDataFromME("Sku", service.flags.AMTTimeoutDuration) + result, err := cmd.GetVersionDataFromME("Sku", service.flags.AMTTimeoutDuration) if err != nil { log.Error(err) } @@ -55,7 +61,7 @@ func (service *ProvisioningService) DisplayAMTInfo() int { } } if service.flags.AmtInfo.UUID { - result, err := amtCommand.GetUUID() + result, err := cmd.GetUUID() if err != nil { log.Error(err) } @@ -66,7 +72,7 @@ func (service *ProvisioningService) DisplayAMTInfo() int { } } if service.flags.AmtInfo.Mode { - result, err := amtCommand.GetControlMode() + result, err := cmd.GetControlMode() if err != nil { log.Error(err) } @@ -77,7 +83,7 @@ func (service *ProvisioningService) DisplayAMTInfo() int { } } if service.flags.AmtInfo.DNS { - result, err := amtCommand.GetDNSSuffix() + result, err := cmd.GetDNSSuffix() if err != nil { log.Error(err) } @@ -86,7 +92,7 @@ func (service *ProvisioningService) DisplayAMTInfo() int { if !service.flags.JsonOutput { println("DNS Suffix : " + string(result)) } - result, err = amtCommand.GetOSDNSSuffix() + result, err = cmd.GetOSDNSSuffix() if err != nil { log.Error(err) } @@ -108,7 +114,7 @@ func (service *ProvisioningService) DisplayAMTInfo() int { } if service.flags.AmtInfo.Ras { - result, err := amtCommand.GetRemoteAccessConnectionStatus() + result, err := cmd.GetRemoteAccessConnectionStatus() if err != nil { log.Error(err) } @@ -122,7 +128,7 @@ func (service *ProvisioningService) DisplayAMTInfo() int { } } if service.flags.AmtInfo.Lan { - wired, err := amtCommand.GetLANInterfaceSettings(false) + wired, err := cmd.GetLANInterfaceSettings(false) if err != nil { log.Error(err) } @@ -137,7 +143,7 @@ func (service *ProvisioningService) DisplayAMTInfo() int { println("MAC Address : " + wired.MACAddress) } - wireless, err := amtCommand.GetLANInterfaceSettings(true) + wireless, err := cmd.GetLANInterfaceSettings(true) if err != nil { log.Error(err) } @@ -153,30 +159,72 @@ func (service *ProvisioningService) DisplayAMTInfo() int { } } if service.flags.AmtInfo.Cert { - result, err := amtCommand.GetCertificateHashes() + result, err := cmd.GetCertificateHashes() if err != nil { log.Error(err) } - certs := make(map[string]interface{}) + sysCertMap := map[string]amt.CertHashEntry{} for _, v := range result { - certs[v.Name] = v + sysCertMap[v.Name] = v } - dataStruct["certificateHashes"] = certs + dataStruct["certificateHashes"] = sysCertMap if !service.flags.JsonOutput { - println("Certificate Hashes :") - for _, v := range result { - print(v.Name + " (") - if v.IsDefault { - print("Default,") + if len(result) == 0 { + fmt.Println("---No Certificate Hashes Found---") + } else { + fmt.Println("---Certificate Hashes---") + } + for k, v := range sysCertMap { + fmt.Printf("%s", k) + if v.IsDefault && v.IsActive { + fmt.Printf(" (Default, Active)") + } else if v.IsDefault { + fmt.Printf(" (Default)") + } else if v.IsActive { + fmt.Printf(" (Active)") } - if v.IsActive { - print("Active)") + fmt.Println() + fmt.Println(" " + v.Algorithm + ": " + v.Hash) + } + } + } + if service.flags.AmtInfo.UserCert { + service.setupWsmanClient("admin", service.flags.Password) + var userCerts []publickey.PublicKeyCertificate + service.GetPublicKeyCerts(&userCerts) + userCertMap := map[string]publickey.PublicKeyCertificate{} + for i := range userCerts { + c := userCerts[i] + name := GetTokenFromKeyValuePairs(c.Subject, "CN") + // CN is not required by spec, but should work + // just in case, provide something accurate + if name == "" { + name = c.InstanceID + } + userCertMap[name] = c + } + dataStruct["publicKeyCerts"] = userCertMap + + if !service.flags.JsonOutput { + if len(userCertMap) == 0 { + fmt.Println("---No Public Key Certs Found---") + } else { + fmt.Println("---Public Key Certs---") + } + for k, c := range userCertMap { + fmt.Printf("%s", k) + if c.TrustedRootCertficate && c.ReadOnlyCertificate { + fmt.Printf(" (TrustedRoot, ReadOnly)") + } else if c.TrustedRootCertficate { + fmt.Printf(" (TrustedRoot)") + } else if c.ReadOnlyCertificate { + fmt.Printf(" (ReadOnly)") } - println() - println(" " + v.Algorithm + ": " + v.Hash) + fmt.Println() } } } + if service.flags.JsonOutput { outBytes, err := json.MarshalIndent(dataStruct, "", " ") output := string(outBytes) diff --git a/internal/local/info_test.go b/internal/local/info_test.go index 3c4045e0..ab2d3456 100644 --- a/internal/local/info_test.go +++ b/internal/local/info_test.go @@ -1,6 +1,8 @@ package local import ( + "github.com/open-amt-cloud-toolkit/go-wsman-messages/pkg/amt/publickey" + "github.com/open-amt-cloud-toolkit/go-wsman-messages/pkg/common" "github.com/stretchr/testify/assert" "rpc/internal/flags" "rpc/pkg/utils" @@ -8,31 +10,55 @@ import ( ) func TestDisplayAMTInfo(t *testing.T) { - f := &flags.Flags{} - f.Command = utils.CommandVersion - f.AmtInfo.Ver = true - f.AmtInfo.Bld = true - f.AmtInfo.Sku = true - f.AmtInfo.UUID = true - f.AmtInfo.Mode = true - f.AmtInfo.DNS = true - f.AmtInfo.Cert = true - f.AmtInfo.Ras = true - f.AmtInfo.Lan = true - f.AmtInfo.Hostname = true + //f := &flags.Flags{} + defaultFlags := flags.AmtInfoFlags{ + Ver: true, + Bld: true, + Sku: true, + UUID: true, + Mode: true, + DNS: true, + Ras: true, + Lan: true, + Hostname: true, + } t.Run("returns Success on happy path", func(t *testing.T) { + f := &flags.Flags{} + f.AmtInfo = defaultFlags lps := setupService(f) resultCode := lps.DisplayAMTInfo() assert.Equal(t, utils.Success, resultCode) }) t.Run("returns Success with json output", func(t *testing.T) { + f := &flags.Flags{} + f.AmtInfo = defaultFlags f.JsonOutput = true lps := setupService(f) resultCode := lps.DisplayAMTInfo() assert.Equal(t, utils.Success, resultCode) - f.JsonOutput = false + }) + + t.Run("returns Success with certs", func(t *testing.T) { + f := &flags.Flags{} + f.AmtInfo.Cert = true + f.AmtInfo.UserCert = true + f.Password = "testPassword" + mockCertHashes = mockCertHashesDefault + pullEnvelope := publickey.PullResponseEnvelope{} + pullEnvelope.Body.PullResponse.Items = []publickey.PublicKeyCertificate{ + mpsCert, + clientCert, + caCert, + } + rfa := ResponseFuncArray{ + respondMsgFunc(t, common.EnumerationResponse{}), + respondMsgFunc(t, pullEnvelope), + } + lps := setupWsmanResponses(t, f, rfa) + resultCode := lps.DisplayAMTInfo() + assert.Equal(t, utils.Success, resultCode) }) t.Run("returns Success but logs errors on error conditions", func(t *testing.T) { @@ -45,7 +71,10 @@ func TestDisplayAMTInfo(t *testing.T) { mockLANInterfaceSettingsErr = mockStandardErr mockCertHashesErr = mockStandardErr + f := &flags.Flags{} + f.AmtInfo = defaultFlags f.JsonOutput = true + lps := setupService(f) resultCode := lps.DisplayAMTInfo() assert.Equal(t, utils.Success, resultCode) diff --git a/internal/local/lps_test.go b/internal/local/lps_test.go index 505d5090..994b1670 100644 --- a/internal/local/lps_test.go +++ b/internal/local/lps_test.go @@ -51,6 +51,27 @@ var mockOSDNSSuffixErr error = nil func (c MockAMT) GetOSDNSSuffix() (string, error) { return mockOSDNSSuffix, mockOSDNSSuffixErr } +var mockCertHashesDefault = []amt2.CertHashEntry{ + { + Hash: "ABCDEFG", + Name: "Cert 01 Big Important CA", + Algorithm: "SHA256", + IsDefault: true, + }, + { + Hash: "424242", + Name: "Cert 02 Small Important CA", + Algorithm: "SHA256", + IsActive: true, + }, + { + Hash: "wiggledywaggledy", + Name: "Cert 03 NotAtAll Important CA", + Algorithm: "SHA256", + IsActive: true, + IsDefault: true, + }, +} var mockCertHashes []amt2.CertHashEntry var mockCertHashesErr error = nil diff --git a/internal/local/utils.go b/internal/local/utils.go index 3064a86d..44b0bc53 100644 --- a/internal/local/utils.go +++ b/internal/local/utils.go @@ -10,6 +10,7 @@ import ( log "github.com/sirupsen/logrus" "reflect" "rpc/pkg/utils" + "strings" ) func reflectObjectName(v any) string { @@ -58,6 +59,16 @@ func (service *ProvisioningService) PostAndUnmarshal(xmlMsg string, outObj any) return utils.Success } +func GetTokenFromKeyValuePairs(kvList string, token string) string { + attributes := strings.Split(kvList, ",") + tokenMap := make(map[string]string) + for _, att := range attributes { + parts := strings.Split(att, "=") + tokenMap[parts[0]] = parts[1] + } + return tokenMap[token] +} + func (service *ProvisioningService) GetPublicKeyCerts(certs *[]publickey.PublicKeyCertificate) int { var pullRspEnv publickey.PullResponseEnvelope diff --git a/internal/local/utils_test.go b/internal/local/utils_test.go index f9511a14..b913f8a3 100644 --- a/internal/local/utils_test.go +++ b/internal/local/utils_test.go @@ -11,21 +11,31 @@ import ( "testing" ) +var mpsCert = publickey.PublicKeyCertificate{ + ElementName: "Intel(r) AMT Certificate", + InstanceID: "Intel(r) AMT Certificate: Handle: 0", + X509Certificate: `MIIEkzCCA3ugAwIBAgIUL3WtF7HfMKxQOHcZy65Z0tsSoLwwDQYJKoZIhvc`, + TrustedRootCertficate: true, + Issuer: "C=unknown,O=unknown,CN=MPSRoot-5bb511", + Subject: "C=unknown,O=unknown,CN=MPSRoot-5bb511", + ReadOnlyCertificate: true, +} var caCert = publickey.PublicKeyCertificate{ ElementName: "Intel(r) AMT Certificate", InstanceID: "Intel(r) AMT Certificate: Handle: 1", - X509Certificate: `MIIEkzCCA3ugAwIBAgIUL3WtF7HfMKxQOHcZy65Z0tsSoLwwDQYJKoZIhvc`, + X509Certificate: `CERTHANDLE1MIIEkzCCA3ugAwIBAgIUL3WtF7HfMKxQOHcZy65Z0tsSoLwwDQYJKoZIhvc`, TrustedRootCertficate: true, Issuer: `C=US,S=Arizona,L=Chandler,CN=Unit Tests Are Us`, Subject: `C=US,S=Arizona,L=Chandler,CN=Unit Test CA Root Certificate`, } var clientCert = publickey.PublicKeyCertificate{ ElementName: "Intel(r) AMT Certificate", - InstanceID: "Intel(r) AMT Certificate: Handle: 1", - X509Certificate: `MIIDjzCCAnegAwIBAgIUBgF0PsmOxA/KJVDCcbW+n5IbemgwDQYJKoZIhvc`, + InstanceID: "Intel(r) AMT Certificate: Handle: 3", + X509Certificate: `CERTHANDLE2AwIBAgIUBgF0PsmOxA/KJVDCcbW+n5IbemgwDQYJKoZIhvc`, TrustedRootCertficate: false, Issuer: `C=US,S=Arizona,L=Chandler,CN=Unit Tests Are Us`, Subject: `C=US,S=Arizona,L=Chandler,CN=Unit Test Client Certificate`, + ReadOnlyCertificate: true, } var keyPair01 = publicprivate.PublicPrivateKeyPair{ ElementName: "Intel(r) AMT Key",