From 248d3e8399538e1f14c541f206a4adf19869c93d Mon Sep 17 00:00:00 2001 From: peytonr18 Date: Tue, 6 Jan 2026 11:50:19 -0800 Subject: [PATCH 1/2] go module updates for crypt support Via the commands: go get github.com/GehirnInc/crypt go mod vendor --- go.mod | 1 + go.sum | 2 ++ vendor/modules.txt | 2 ++ 3 files changed, 5 insertions(+) diff --git a/go.mod b/go.mod index 712e4a75b..56b53586e 100644 --- a/go.mod +++ b/go.mod @@ -45,6 +45,7 @@ require ( github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 // indirect + github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0 // indirect diff --git a/go.sum b/go.sum index 8640313b2..050d61f9f 100644 --- a/go.sum +++ b/go.sum @@ -36,6 +36,8 @@ github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJ github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE= github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs= github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk= +github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5 h1:IEjq88XO4PuBDcvmjQJcQGg+w+UaafSy8G5Kcb5tBhI= +github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5/go.mod h1:exZ0C/1emQJAw5tHOaUDyY1ycttqBAPcxuzf7QbY6ec= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 h1:sBEjpZlNHzK1voKq9695PJSX2o5NEXl7/OL3coiIY0c= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0 h1:lhhYARPUu3LmHysQ/igznQphfzynnqI3D75oUyw1HXk= diff --git a/vendor/modules.txt b/vendor/modules.txt index 7c78db556..136f696c5 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -123,6 +123,8 @@ github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/shared github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/version github.com/AzureAD/microsoft-authentication-library-for-go/apps/managedidentity github.com/AzureAD/microsoft-authentication-library-for-go/apps/public +# github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5 +## explicit; go 1.19 # github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 ## explicit; go 1.24.0 github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp From ab3b0af8fae511dc512a2922321aec46f81722ea Mon Sep 17 00:00:00 2001 From: peytonr18 Date: Tue, 6 Jan 2026 14:59:46 -0800 Subject: [PATCH 2/2] azure: add --generate-cloud-config flag for dynamic config synthesis Add the ability to synthesize Ignition configurations dynamically from Azure Instance Metadata Service (IMDS) and OVF provisioning data. --- dracut/30ignition/ignition-fetch.service | 2 +- go.mod | 2 +- internal/exec/engine.go | 43 +- internal/main.go | 44 +- internal/platform/platform.go | 10 + internal/providers/azure/azure.go | 516 ++++++++++++++++-- internal/providers/azure/azure_test.go | 490 +++++++++++++++++ internal/providers/azure/password.go | 84 +++ vendor/github.com/GehirnInc/crypt/.travis.yml | 7 + vendor/github.com/GehirnInc/crypt/AUTHORS.md | 8 + vendor/github.com/GehirnInc/crypt/LICENSE | 26 + vendor/github.com/GehirnInc/crypt/README.rst | 61 +++ .../GehirnInc/crypt/common/base64.go | 59 ++ .../github.com/GehirnInc/crypt/common/doc.go | 10 + .../github.com/GehirnInc/crypt/common/salt.go | 148 +++++ vendor/github.com/GehirnInc/crypt/crypt.go | 121 ++++ .../GehirnInc/crypt/internal/utils.go | 41 ++ .../crypt/sha512_crypt/sha512_crypt.go | 188 +++++++ vendor/modules.txt | 4 + 19 files changed, 1781 insertions(+), 83 deletions(-) create mode 100644 internal/providers/azure/azure_test.go create mode 100644 internal/providers/azure/password.go create mode 100644 vendor/github.com/GehirnInc/crypt/.travis.yml create mode 100644 vendor/github.com/GehirnInc/crypt/AUTHORS.md create mode 100644 vendor/github.com/GehirnInc/crypt/LICENSE create mode 100644 vendor/github.com/GehirnInc/crypt/README.rst create mode 100644 vendor/github.com/GehirnInc/crypt/common/base64.go create mode 100644 vendor/github.com/GehirnInc/crypt/common/doc.go create mode 100644 vendor/github.com/GehirnInc/crypt/common/salt.go create mode 100644 vendor/github.com/GehirnInc/crypt/crypt.go create mode 100644 vendor/github.com/GehirnInc/crypt/internal/utils.go create mode 100644 vendor/github.com/GehirnInc/crypt/sha512_crypt/sha512_crypt.go diff --git a/dracut/30ignition/ignition-fetch.service b/dracut/30ignition/ignition-fetch.service index fb36f1af7..489177320 100644 --- a/dracut/30ignition/ignition-fetch.service +++ b/dracut/30ignition/ignition-fetch.service @@ -25,4 +25,4 @@ After=network.target Type=oneshot RemainAfterExit=yes EnvironmentFile=/run/ignition.env -ExecStart=/usr/bin/ignition --root=/sysroot --platform=${PLATFORM_ID} --stage=fetch ${IGNITION_ARGS} +ExecStart=/usr/bin/ignition --root=/sysroot --platform=${PLATFORM_ID} --stage=fetch --generate-cloud-config=true ${IGNITION_ARGS} diff --git a/go.mod b/go.mod index 56b53586e..f352433c4 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( cloud.google.com/go/storage v1.58.0 github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.3 + github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5 github.com/aws/aws-sdk-go-v2 v1.41.0 github.com/aws/aws-sdk-go-v2/credentials v1.19.6 github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16 @@ -45,7 +46,6 @@ require ( github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 // indirect - github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0 // indirect diff --git a/internal/exec/engine.go b/internal/exec/engine.go index c2e61f7eb..f172f12ad 100644 --- a/internal/exec/engine.go +++ b/internal/exec/engine.go @@ -55,14 +55,15 @@ var ( // Engine represents the entity that fetches and executes a configuration. type Engine struct { - ConfigCache string - FetchTimeout time.Duration - Logger *log.Logger - NeedNet string - Root string - PlatformConfig platform.Config - Fetcher *resource.Fetcher - State *state.State + ConfigCache string + FetchTimeout time.Duration + GenerateCloudConfig bool + Logger *log.Logger + NeedNet string + Root string + PlatformConfig platform.Config + Fetcher *resource.Fetcher + State *state.State } // Run executes the stage of the given name. It returns true if the stage @@ -286,6 +287,10 @@ func (e *Engine) acquireProviderConfig() (cfg types.Config, err error) { // is unavailable. This will also render the config (see renderConfig) before // returning. func (e *Engine) fetchProviderConfig() (types.Config, error) { + if e.GenerateCloudConfig { + return e.fetchGeneratedConfig() + } + platformConfigs := []platform.Config{ cmdline.Config, system.Config, @@ -331,6 +336,28 @@ func (e *Engine) fetchProviderConfig() (types.Config, error) { return configFetcher.RenderConfig(cfg) } +func (e *Engine) fetchGeneratedConfig() (types.Config, error) { + e.Logger.Info("using generated cloud config for platform %q", e.PlatformConfig.Name()) + cfg, err := e.PlatformConfig.GenerateConfig(e.Fetcher) + if err != nil { + return types.Config{}, err + } + + e.State.FetchedConfigs = append(e.State.FetchedConfigs, state.FetchedConfig{ + Kind: "user", + Source: fmt.Sprintf("%s-generator", e.PlatformConfig.Name()), + Referenced: false, + }) + + configFetcher := ConfigFetcher{ + Logger: e.Logger, + Fetcher: e.Fetcher, + State: e.State, + } + + return configFetcher.RenderConfig(cfg) +} + func (e *Engine) signalNeedNet() error { if err := executil.MkdirForFile(e.NeedNet); err != nil { return err diff --git a/internal/main.go b/internal/main.go index 4d636a399..41139c197 100644 --- a/internal/main.go +++ b/internal/main.go @@ -48,20 +48,22 @@ func main() { func ignitionMain() { flags := struct { - configCache string - fetchTimeout time.Duration - needNet string - platform platform.Name - root string - stage stages.Name - stateFile string - version bool - logToStdout bool + configCache string + fetchTimeout time.Duration + generateCloudConfig bool + needNet string + platform platform.Name + root string + stage stages.Name + stateFile string + version bool + logToStdout bool }{} flag.StringVar(&flags.configCache, "config-cache", "/run/ignition.json", "where to cache the config") flag.DurationVar(&flags.fetchTimeout, "fetch-timeout", exec.DefaultFetchTimeout, "initial duration for which to wait for config") flag.StringVar(&flags.needNet, "neednet", "/run/ignition/neednet", "flag file to write from fetch-offline if networking is needed") + flag.BoolVar(&flags.generateCloudConfig, "generate-cloud-config", false, "generate config from cloud provider metadata instead of fetching") flag.Var(&flags.platform, "platform", fmt.Sprintf("current platform. %v", platform.Names())) flag.StringVar(&flags.root, "root", "/", "root of the filesystem") flag.Var(&flags.stage, "stage", fmt.Sprintf("execution stage. %v", stages.Names())) @@ -71,6 +73,11 @@ func ignitionMain() { flag.Parse() + // Never allow cloud config generation during fetch-offline stage (no networking) + if flags.stage == "fetch" && flags.platform == "azure" { + flags.generateCloudConfig = true + } + if flags.version { fmt.Printf("%s\n", version.String) return @@ -91,6 +98,8 @@ func ignitionMain() { logger.Info("%s", version.String) logger.Info("Stage: %v", flags.stage) + logger.Info("Platform: %v", flags.platform) + logger.Info("GenerateCloudConfig: %v", flags.generateCloudConfig) platformConfig := platform.MustGet(flags.platform.String()) fetcher, err := platformConfig.NewFetcher(&logger) @@ -104,14 +113,15 @@ func ignitionMain() { os.Exit(3) } engine := exec.Engine{ - Root: flags.root, - FetchTimeout: flags.fetchTimeout, - Logger: &logger, - NeedNet: flags.needNet, - ConfigCache: flags.configCache, - PlatformConfig: platformConfig, - Fetcher: &fetcher, - State: &state, + Root: flags.root, + FetchTimeout: flags.fetchTimeout, + GenerateCloudConfig: flags.generateCloudConfig, + Logger: &logger, + NeedNet: flags.needNet, + ConfigCache: flags.configCache, + PlatformConfig: platformConfig, + Fetcher: &fetcher, + State: &state, } err = engine.Run(flags.stage.String()) diff --git a/internal/platform/platform.go b/internal/platform/platform.go index 9b01df0cc..74c472720 100644 --- a/internal/platform/platform.go +++ b/internal/platform/platform.go @@ -49,6 +49,9 @@ type Provider struct { Status func(stageName string, f resource.Fetcher, e error) error DelConfig func(f *resource.Fetcher) error + // Generates a platform-specific Ignition config from cloud provider metadata. + GenerateCloudConfig func(f *resource.Fetcher) (types.Config, error) + // Fetch, and also save output files to be written during files stage. // Avoid, unless you're certain you need it. FetchWithFiles func(f *resource.Fetcher) ([]types.File, types.Config, report.Report, error) @@ -104,6 +107,13 @@ func (c Config) DelConfig(f *resource.Fetcher) error { } } +func (c Config) GenerateConfig(f *resource.Fetcher) (types.Config, error) { + if c.p.GenerateCloudConfig != nil { + return c.p.GenerateCloudConfig(f) + } + return types.Config{}, ErrNoProvider +} + var configs = registry.Create("platform configs") func Register(provider Provider) { diff --git a/internal/providers/azure/azure.go b/internal/providers/azure/azure.go index 09de9591f..7286e09cd 100644 --- a/internal/providers/azure/azure.go +++ b/internal/providers/azure/azure.go @@ -18,21 +18,28 @@ package azure import ( "encoding/base64" + "encoding/json" + "encoding/xml" + "errors" "fmt" "net/http" "net/url" "os" "path/filepath" + "strconv" + "strings" "time" "github.com/Azure/azure-sdk-for-go/sdk/azidentity" - "github.com/coreos/ignition/v2/config/shared/errors" + configErrors "github.com/coreos/ignition/v2/config/shared/errors" + cfgutil "github.com/coreos/ignition/v2/config/util" "github.com/coreos/ignition/v2/config/v3_6_experimental/types" execUtil "github.com/coreos/ignition/v2/internal/exec/util" "github.com/coreos/ignition/v2/internal/log" "github.com/coreos/ignition/v2/internal/platform" "github.com/coreos/ignition/v2/internal/providers/util" "github.com/coreos/ignition/v2/internal/resource" + "github.com/vincent-petithory/dataurl" "github.com/coreos/vcontext/report" "golang.org/x/sys/unix" @@ -68,13 +75,32 @@ var ( Path: "metadata/instance/compute/userData", RawQuery: "api-version=2021-01-01&format=text", } + + imdsInstanceURL = url.URL{ + Scheme: "http", + Host: "169.254.169.254", + Path: "metadata/instance", + RawQuery: "api-version=2021-03-01&format=json&extended=true", + } +) + +var imdsRetryCodes = []int{ + 404, + 410, + 429, +} + +var ( + fetchInstanceMetadataFunc = fetchInstanceMetadata + readOvfEnvironmentFunc = readOvfEnvironment ) func init() { platform.Register(platform.Provider{ - Name: "azure", - NewFetcher: newFetcher, - Fetch: fetchConfig, + Name: "azure", + NewFetcher: newFetcher, + Fetch: fetchConfig, + GenerateCloudConfig: generateCloudConfig, }) } @@ -110,23 +136,38 @@ func fetchFromAzureMetadata(f *resource.Fetcher) (types.Config, report.Report, e logger := f.Logger - // we first try to fetch config from IMDS, in case of failure we fallback on the custom data. - userData, err := fetchFromIMDS(f) - if err == nil { - logger.Info("config has been read from IMDS userdata") - return util.ParseConfig(logger, userData) - } - - if err != errors.ErrEmpty { - return types.Config{}, report.Report{}, err + // We first try to fetch config from IMDS. If it fails, retry a few times + // before falling back to custom data from the OVF media. + logger.Info("azure: attempting to read userdata from IMDS") + const maxUserdataRetries = 10 + const userdataRetryDelay = 2 * time.Second + + var userData []byte + var err error + for attempt := 0; attempt < maxUserdataRetries; attempt++ { + userData, err = fetchFromIMDS(f) + if err == nil { + logger.Info("config has been read from IMDS userdata") + return util.ParseConfig(logger, userData) + } + if errors.Is(err, configErrors.ErrEmpty) { + logger.Info("azure: IMDS userdata was empty, falling back to custom data") + break + } + if attempt < maxUserdataRetries-1 { + logger.Info("azure: IMDS userdata request failed, retrying in %v (attempt %d/%d): %v", userdataRetryDelay, attempt+1, maxUserdataRetries, err) + time.Sleep(userdataRetryDelay) + continue + } + logger.Warning("azure: IMDS userdata request failed after %d attempts, falling back to custom data: %v", maxUserdataRetries, err) } - logger.Debug("failed to retrieve userdata from IMDS, falling back to custom data: %v", err) return FetchFromOvfDevice(f, []string{CDS_FSTYPE_UDF}) } // fetchFromIMDS requests the Azure IMDS to fetch userdata and decode it. func fetchFromIMDS(f *resource.Fetcher) ([]byte, error) { + logger := f.Logger headers := make(http.Header) headers.Set("Metadata", "true") @@ -134,12 +175,9 @@ func fetchFromIMDS(f *resource.Fetcher) ([]byte, error) { // Here, we match the cloud-init set. // https://github.com/canonical/cloud-init/commit/c1a2047cf291 // https://github.com/coreos/ignition/issues/1806 - retryCodes := []int{ - 404, // not found - 410, // gone - 429, // rate-limited - } - data, err := f.FetchToBuffer(imdsUserdataURL, resource.FetchOptions{Headers: headers, RetryCodes: retryCodes}) + logger.Debug("azure: requesting IMDS userdata from %s", imdsUserdataURL.String()) + data, err := f.FetchToBuffer(imdsUserdataURL, resource.FetchOptions{Headers: headers, RetryCodes: imdsRetryCodes}) + if err != nil { return nil, fmt.Errorf("fetching to buffer: %w", err) } @@ -147,7 +185,7 @@ func fetchFromIMDS(f *resource.Fetcher) ([]byte, error) { n := len(data) if n == 0 { - return nil, errors.ErrEmpty + return nil, configErrors.ErrEmpty } // data is base64 encoded by the IMDS @@ -200,42 +238,8 @@ func FetchFromOvfDevice(f *resource.Fetcher, ovfFsTypes []string) (types.Config, // getRawConfig returns the config by mounting the given block device func getRawConfig(f *resource.Fetcher, devicePath string, fstype string) ([]byte, error) { logger := f.Logger - mnt, err := os.MkdirTemp("", "ignition-azure") - if err != nil { - return nil, fmt.Errorf("failed to create temp directory: %v", err) - } - defer func() { - if removeErr := os.Remove(mnt); removeErr != nil { - logger.Warning("failed to remove temp directory %q: %v", mnt, removeErr) - } - }() - - logger.Debug("mounting config device") - if err := logger.LogOp( - func() error { return unix.Mount(devicePath, mnt, fstype, unix.MS_RDONLY, "") }, - "mounting %q at %q", devicePath, mnt, - ); err != nil { - return nil, fmt.Errorf("failed to mount device %q at %q: %v", devicePath, mnt, err) - } - defer func() { - _ = logger.LogOp( - func() error { return unix.Unmount(mnt, 0) }, - "unmounting %q at %q", devicePath, mnt, - ) - }() - - // detect the config drive by looking for a file which is always present - logger.Debug("checking for config drive") - if _, err := os.Stat(filepath.Join(mnt, "ovf-env.xml")); err != nil { - return nil, fmt.Errorf("device %q does not appear to be a config drive: %v", devicePath, err) - } - logger.Debug("reading config") - rawConfig, err := os.ReadFile(filepath.Join(mnt, configPath)) - if err != nil && !os.IsNotExist(err) { - return nil, fmt.Errorf("failed to read config from device %q: %v", devicePath, err) - } - return rawConfig, nil + return readFileFromDevice(f, devicePath, fstype, configPath) } // isCdromPresent verifies if the given config drive is CD-ROM @@ -275,3 +279,403 @@ func isCdromPresent(logger *log.Logger, devicePath string) bool { return (status == CDS_DISC_OK) } + +func readFileFromDevice(f *resource.Fetcher, devicePath string, fstype string, relativePath string) ([]byte, error) { + logger := f.Logger + mnt, err := os.MkdirTemp("", "ignition-azure") + if err != nil { + return nil, fmt.Errorf("failed to create temp directory: %v", err) + } + defer func() { + if removeErr := os.Remove(mnt); removeErr != nil { + logger.Warning("failed to remove temp directory %q: %v", mnt, removeErr) + } + }() + + logger.Debug("mounting config device") + if err := logger.LogOp( + func() error { return unix.Mount(devicePath, mnt, fstype, unix.MS_RDONLY, "") }, + "mounting %q at %q", devicePath, mnt, + ); err != nil { + return nil, fmt.Errorf("failed to mount device %q at %q: %v", devicePath, mnt, err) + } + defer func() { + _ = logger.LogOp( + func() error { return unix.Unmount(mnt, 0) }, + "unmounting %q at %q", devicePath, mnt, + ) + }() + + logger.Debug("checking for config drive") + if _, err := os.Stat(filepath.Join(mnt, "ovf-env.xml")); err != nil { + return nil, fmt.Errorf("device %q does not appear to be a config drive: %v", devicePath, err) + } + + target := filepath.Join(mnt, strings.TrimPrefix(relativePath, "/")) + data, err := os.ReadFile(target) + if err != nil { + if os.IsNotExist(err) { + return nil, nil + } + return nil, fmt.Errorf("failed to read %q from device %q: %v", relativePath, devicePath, err) + } + return data, nil +} + +type instanceMetadata struct { + Compute instanceComputeMetadata `json:"compute"` +} + +type instanceComputeMetadata struct { + Hostname string `json:"hostname"` + OSProfile instanceOSProfile `json:"osProfile"` + PublicKeys []instancePublicKey `json:"publicKeys"` +} + +type instanceOSProfile struct { + AdminUsername string `json:"adminUsername"` +} + +type instancePublicKey struct { + KeyData string `json:"keyData"` +} + +type provisioningEnvelope struct { + LinuxProvisioningConfigurationSet linuxProvisioningConfigurationSet `xml:"LinuxProvisioningConfigurationSet"` +} + +type linuxProvisioningConfigurationSet struct { + HostName string `xml:"HostName"` + UserName string `xml:"UserName"` + UserPassword string `xml:"UserPassword"` + DisableSshPasswordAuthentication string `xml:"DisableSshPasswordAuthentication"` + SSH sshSection `xml:"SSH"` + CustomData string `xml:"CustomData"` + UserData string `xml:"UserData"` +} + +type sshSection struct { + PublicKeys []sshPublicKey `xml:"PublicKeys>PublicKey"` +} + +type sshPublicKey struct { + Value string `xml:"Value"` +} + +func (l linuxProvisioningConfigurationSet) passwordAuthDisabled() bool { + val := strings.ToLower(strings.TrimSpace(l.DisableSshPasswordAuthentication)) + switch val { + case "true", "1", "yes": + return true + case "false", "0", "no", "": + return false + default: + // Try parsing as bool for any other values + disabled, err := strconv.ParseBool(val) + if err != nil { + return false + } + return disabled + } +} + +func generateCloudConfig(f *resource.Fetcher) (types.Config, error) { + logger := f.Logger + logger.Info("azure: [1/4] generating cloud config via IMDS + OVF metadata") + logger.Info("azure: [2/4] requesting instance metadata from IMDS") + meta, metaErr := fetchInstanceMetadataFunc(f) + if metaErr != nil { + metaErr = fmt.Errorf("fetching instance metadata: %w", metaErr) + logger.Warning("azure: failed to fetch instance metadata from IMDS: %v", metaErr) + meta = nil + } else { + logger.Info("azure: fetched instance metadata from IMDS: %+v", meta) + } + + logger.Info("azure: [3/4] reading OVF provisioning metadata from attached media") + ovfRaw, err := readOvfEnvironmentFunc(f, []string{CDS_FSTYPE_UDF}) + var provisioningErr error + if err != nil { + provisioningErr = fmt.Errorf("reading OVF provisioning metadata: %w", err) + logger.Warning("azure: failed to read OVF provisioning metadata: %v", provisioningErr) + ovfRaw = nil + } else if len(ovfRaw) == 0 { + logger.Warning("azure: ovf-env.xml was empty") + ovfRaw = nil + } else { + logger.Info("azure: read provisioning metadata from OVF (bytes=%d)", len(ovfRaw)) + } + + logger.Info("azure: [4/4] parsing provisioning metadata and synthesizing Ignition config") + var provisioning *linuxProvisioningConfigurationSet + if ovfRaw != nil { + provisioning, err = parseProvisioningConfig(ovfRaw) + if err != nil { + provisioningErr = fmt.Errorf("parsing OVF provisioning metadata: %w", err) + logger.Warning("azure: failed to parse provisioning metadata: %v", provisioningErr) + provisioning = nil + } else { + logger.Info("azure: successfully parsed provisioning metadata from ovfRaw") + } + } + + // Log summary of available data before building config + logger.Info("azure: data summary before config generation:") + logger.Info("azure: IMDS metadata available: %v", meta != nil) + logger.Info("azure: OVF provisioning available: %v", provisioning != nil) + if meta != nil { + logger.Info("azure: IMDS username: %q", meta.Compute.OSProfile.AdminUsername) + logger.Info("azure: IMDS SSH keys count: %d", len(meta.Compute.PublicKeys)) + } + if provisioning != nil { + logger.Info("azure: OVF username: %q", provisioning.UserName) + logger.Info("azure: OVF has password: %v", provisioning.UserPassword != "") + logger.Info("azure: OVF SSH keys count: %d", len(provisioning.SSH.PublicKeys)) + } + + if meta == nil && provisioning == nil { + logger.Warning("azure: both IMDS and OVF data are unavailable - config generation will likely fail") + switch { + case metaErr != nil: + return types.Config{}, metaErr + case provisioningErr != nil: + return types.Config{}, provisioningErr + default: + return types.Config{}, fmt.Errorf("azure: no instance metadata or provisioning data available") + } + } + + cfg, err := buildGeneratedConfig(logger, meta, provisioning) + if err != nil { + logger.Warning("azure: failed to build generated config: %v", err) + return types.Config{}, err + } + + logger.Info("azure: generated cloud config successfully") + logger.Info("azure: config includes user %q with %d SSH keys", cfg.Passwd.Users[0].Name, len(cfg.Passwd.Users[0].SSHAuthorizedKeys)) + return cfg, nil +} + +func fetchInstanceMetadata(f *resource.Fetcher) (*instanceMetadata, error) { + logger := f.Logger + headers := make(http.Header) + headers.Set("Metadata", "true") + + // Retry IMDS metadata fetch if networking isn't ready yet + const maxNetRetries = 10 + const netRetryDelay = 2 * time.Second + + var data []byte + var err error + + for attempt := 0; attempt < maxNetRetries; attempt++ { + data, err = f.FetchToBuffer(imdsInstanceURL, resource.FetchOptions{Headers: headers, RetryCodes: imdsRetryCodes}) + if err == nil { + break + } + + if attempt < maxNetRetries-1 { + logger.Info("azure: IMDS request failed, retrying in %v (attempt %d/%d): %v", netRetryDelay, attempt+1, maxNetRetries, err) + time.Sleep(netRetryDelay) + continue + } + logger.Warning("azure: IMDS failed after %d attempts: %v", maxNetRetries, err) + } + + if err != nil { + return nil, fmt.Errorf("fetching metadata: %w", err) + } + + var meta instanceMetadata + if err := json.Unmarshal(data, &meta); err != nil { + return nil, fmt.Errorf("decoding metadata: %w", err) + } + return &meta, nil +} + +const ( + // maxOvfRetries is the maximum number of attempts to find the OVF environment + maxOvfRetries = 30 + // ovfRetryInterval is the time between retries + ovfRetryInterval = time.Second +) + +func readOvfEnvironment(f *resource.Fetcher, ovfFsTypes []string) ([]byte, error) { + logger := f.Logger + checkedDevices := make(map[string]struct{}) + + for attempt := 0; attempt < maxOvfRetries; attempt++ { + for _, ovfFsType := range ovfFsTypes { + devices, err := execUtil.GetBlockDevices(ovfFsType) + if err != nil { + return nil, fmt.Errorf("failed to retrieve block devices with FSTYPE=%q: %v", ovfFsType, err) + } + for _, dev := range devices { + if _, checked := checkedDevices[dev]; checked { + continue + } + if isCdromPresent(logger, dev) { + data, err := readFileFromDevice(f, dev, ovfFsType, "ovf-env.xml") + if err != nil { + logger.Debug("failed to read ovf environment from device %q: %v", dev, err) + } else if len(data) > 0 { + return data, nil + } + } + checkedDevices[dev] = struct{}{} + } + } + if attempt < maxOvfRetries-1 { + time.Sleep(ovfRetryInterval) + } + } + + return nil, fmt.Errorf("failed to find OVF environment after %d attempts", maxOvfRetries) +} + +func parseProvisioningConfig(raw []byte) (*linuxProvisioningConfigurationSet, error) { + var env provisioningEnvelope + if err := xml.Unmarshal(raw, &env); err != nil { + return nil, err + } + return &env.LinuxProvisioningConfigurationSet, nil +} + +func buildGeneratedConfig(logger *log.Logger, meta *instanceMetadata, provisioning *linuxProvisioningConfigurationSet) (types.Config, error) { + var username string + if meta != nil { + username = strings.TrimSpace(meta.Compute.OSProfile.AdminUsername) + } + if username == "" && provisioning != nil { + username = strings.TrimSpace(provisioning.UserName) + } + if username == "" { + return types.Config{}, fmt.Errorf("unable to determine admin username: IMDS returned empty/nil, OVF returned empty/nil") + } + + var password string + var passwordAuthDisabled bool + if provisioning != nil { + password = strings.TrimSpace(provisioning.UserPassword) + passwordAuthDisabled = provisioning.passwordAuthDisabled() + } + + sshKeys := collectSSHPublicKeys(meta, provisioning) + + user := types.PasswdUser{ + Name: username, + Groups: []types.Group{"wheel"}, + HomeDir: cfgutil.StrToPtr(fmt.Sprintf("/home/%s", username)), + Shell: cfgutil.StrToPtr("/bin/bash"), + SSHAuthorizedKeys: sshKeys, + } + if password != "" { + // Hash the password if it's not already hashed + var passwordHash string + if IsPasswordHashed(password) { + passwordHash = password + } else { + var err error + passwordHash, err = HashPassword(password) + if err != nil { + return types.Config{}, fmt.Errorf("hashing password: %w", err) + } + } + user.PasswordHash = cfgutil.StrToPtr(passwordHash) + } + + sudoersFile := newDataFile("/etc/sudoers.d/50-azure-cloud-config", 0440, "%wheel ALL=(ALL) NOPASSWD: ALL\n") + passwordSetting := "no" + if password != "" && !passwordAuthDisabled { + passwordSetting = "yes" + } + sshConfig := fmt.Sprintf(`# Custom SSHD settings +PasswordAuthentication %s +PermitRootLogin no +AllowUsers %s +`, passwordSetting, username) + sshdFile := newDataFile("/etc/ssh/sshd_config.d/50-azure-cloud-config.conf", 0644, sshConfig) + + files := []types.File{sudoersFile, sshdFile} + files = append(files, provisioningDataFiles(logger, provisioning)...) + + return types.Config{ + Ignition: types.Ignition{ + Version: types.MaxVersion.String(), + }, + Passwd: types.Passwd{ + Users: []types.PasswdUser{user}, + }, + Storage: types.Storage{ + Files: files, + }, + }, nil +} + +func provisioningDataFiles(logger *log.Logger, provisioning *linuxProvisioningConfigurationSet) []types.File { + if provisioning == nil { + return nil + } + + var files []types.File + if data := strings.TrimSpace(provisioning.CustomData); data != "" { + decoded, err := base64.StdEncoding.DecodeString(data) + if err != nil { + if logger != nil { + logger.Warning("azure: failed to decode provisioning CustomData, storing raw value: %v", err) + } + files = append(files, newDataFile("/var/lib/waagent/CustomData", 0600, data)) + } else { + files = append(files, newDataFile("/var/lib/waagent/CustomData", 0600, string(decoded))) + } + } + + if data := strings.TrimSpace(provisioning.UserData); data != "" { + files = append(files, newDataFile("/var/lib/waagent/UserData", 0600, data)) + } + + return files +} + +func collectSSHPublicKeys(meta *instanceMetadata, provisioning *linuxProvisioningConfigurationSet) []types.SSHAuthorizedKey { + seen := make(map[string]struct{}) + var keys []types.SSHAuthorizedKey + + addKey := func(keyData string) { + key := strings.TrimSpace(keyData) + if key == "" { + return + } + if _, ok := seen[key]; !ok { + seen[key] = struct{}{} + keys = append(keys, types.SSHAuthorizedKey(key)) + } + } + + if meta != nil { + for _, k := range meta.Compute.PublicKeys { + addKey(k.KeyData) + } + } + + if provisioning != nil { + for _, pk := range provisioning.SSH.PublicKeys { + addKey(pk.Value) + } + } + + return keys +} + +func newDataFile(path string, mode int, contents string) types.File { + encoded := dataurl.EncodeBytes([]byte(contents)) + return types.File{ + Node: types.Node{ + Path: path, + }, + FileEmbedded1: types.FileEmbedded1{ + Mode: cfgutil.IntToPtr(mode), + Contents: types.Resource{Source: &encoded}, + }, + } +} diff --git a/internal/providers/azure/azure_test.go b/internal/providers/azure/azure_test.go new file mode 100644 index 000000000..a12da4458 --- /dev/null +++ b/internal/providers/azure/azure_test.go @@ -0,0 +1,490 @@ +// Copyright 2015 CoreOS, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package azure + +import ( + "fmt" + "net/url" + "strings" + "testing" + + "github.com/coreos/ignition/v2/config/v3_6_experimental/types" + "github.com/coreos/ignition/v2/internal/log" + "github.com/coreos/ignition/v2/internal/resource" + "github.com/vincent-petithory/dataurl" +) + +type stubFetcher struct { + resource.Fetcher + responses map[string][]byte +} + +func newStubFetcher() *stubFetcher { + l := log.New(true) + return &stubFetcher{ + Fetcher: resource.Fetcher{Logger: &l}, + responses: make(map[string][]byte), + } +} + +func (f *stubFetcher) expect(url string, payload []byte) { + f.responses[url] = payload +} + +func (f *stubFetcher) FetchToBuffer(u url.URL, opts resource.FetchOptions) ([]byte, error) { + if data, ok := f.responses[u.String()]; ok { + return data, nil + } + return nil, fmt.Errorf("unexpected URL %s", u.String()) +} + +func testLogger(t *testing.T) *log.Logger { + t.Helper() + logger := log.New(true) + t.Cleanup(func() { + logger.Close() + }) + return &logger +} + +func fileByPath(t *testing.T, files []types.File, path string) *types.File { + t.Helper() + for i := range files { + if files[i].Node.Path == path { + return &files[i] + } + } + t.Fatalf("file %s not found", path) + return nil +} + +func dataURLContents(t *testing.T, src string) string { + t.Helper() + du, err := dataurl.DecodeString(src) + if err != nil { + t.Fatalf("failed to decode data URL: %v", err) + } + return string(du.Data) +} + +func TestParseProvisioningConfig(t *testing.T) { + raw := []byte(` + + + myhost + azureuser + password + false + + + + ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCu + + + + +`) + + cfg, err := parseProvisioningConfig(raw) + if err != nil { + t.Fatalf("parseProvisioningConfig() err = %v", err) + } + if cfg.UserName != "azureuser" { + t.Fatalf("expected username azureuser, got %s", cfg.UserName) + } + if len(cfg.SSH.PublicKeys) != 1 { + t.Fatalf("expected 1 ssh key, got %d", len(cfg.SSH.PublicKeys)) + } +} + +func TestParseProvisioningConfigErrors(t *testing.T) { + tests := []struct { + name string + xml []byte + }{ + { + name: "malformed XML", + xml: []byte(` + + testuser + + `), + }, + { + name: "empty XML", + xml: []byte(``), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := parseProvisioningConfig(tt.xml) + if err == nil { + t.Fatalf("expected error for %s", tt.name) + } + }) + } +} + +func TestBuildGeneratedConfig(t *testing.T) { + meta := &instanceMetadata{ + Compute: instanceComputeMetadata{ + Hostname: "example", + OSProfile: instanceOSProfile{ + AdminUsername: "meta-user", + }, + PublicKeys: []instancePublicKey{ + {KeyData: "ssh-rsa AAAAB3Nza meta"}, + }, + }, + } + prov := &linuxProvisioningConfigurationSet{ + UserName: "prov-user", + SSH: sshSection{ + PublicKeys: []sshPublicKey{ + {Value: "ssh-ed25519 AAAAC3Nza prov"}, + }, + }, + UserPassword: "plaintext", + } + + cfg, err := buildGeneratedConfig(testLogger(t), meta, prov) + if err != nil { + t.Fatalf("buildGeneratedConfig() err = %v", err) + } + + if len(cfg.Passwd.Users) != 1 { + t.Fatalf("expected 1 user, got %d", len(cfg.Passwd.Users)) + } + user := cfg.Passwd.Users[0] + if user.Name != "meta-user" { + t.Fatalf("expected user meta-user, got %s", user.Name) + } + if len(user.SSHAuthorizedKeys) != 2 { + t.Fatalf("expected 2 ssh keys, got %d", len(user.SSHAuthorizedKeys)) + } + // Password should be hashed (starts with $6$ for SHA-512) + if user.PasswordHash == nil { + t.Fatalf("expected password hash to be set") + } + if !strings.HasPrefix(*user.PasswordHash, "$6$") { + t.Fatalf("expected password hash to be SHA-512 (start with $6$), got %s", *user.PasswordHash) + } + + if len(cfg.Storage.Files) != 2 { + t.Fatalf("expected 2 files, got %d", len(cfg.Storage.Files)) + } +} + +func TestBuildGeneratedConfigWithPrehashedPassword(t *testing.T) { + // Test that pre-hashed passwords are not double-hashed + prehashedPassword := "$6$rounds=5000$saltsalt$hashedvalue" + meta := &instanceMetadata{ + Compute: instanceComputeMetadata{ + OSProfile: instanceOSProfile{ + AdminUsername: "testuser", + }, + }, + } + prov := &linuxProvisioningConfigurationSet{ + UserPassword: prehashedPassword, + } + + cfg, err := buildGeneratedConfig(testLogger(t), meta, prov) + if err != nil { + t.Fatalf("buildGeneratedConfig() err = %v", err) + } + + user := cfg.Passwd.Users[0] + if user.PasswordHash == nil || *user.PasswordHash != prehashedPassword { + t.Fatalf("expected pre-hashed password to be preserved, got %v", user.PasswordHash) + } +} + +func TestBuildGeneratedConfigErrors(t *testing.T) { + meta := &instanceMetadata{} + prov := &linuxProvisioningConfigurationSet{} + if _, err := buildGeneratedConfig(testLogger(t), meta, prov); err == nil { + t.Fatalf("expected error when username missing") + } +} + +func TestBuildGeneratedConfigUsernamePriority(t *testing.T) { + // Test that IMDS AdminUsername takes priority over OVF UserName + meta := &instanceMetadata{ + Compute: instanceComputeMetadata{ + OSProfile: instanceOSProfile{ + AdminUsername: "imds-admin", + }, + }, + } + prov := &linuxProvisioningConfigurationSet{ + UserName: "ovf-user", + } + + cfg, err := buildGeneratedConfig(testLogger(t), meta, prov) + if err != nil { + t.Fatalf("buildGeneratedConfig() err = %v", err) + } + if cfg.Passwd.Users[0].Name != "imds-admin" { + t.Fatalf("expected IMDS username 'imds-admin' to take priority, got %s", cfg.Passwd.Users[0].Name) + } +} + +func TestBuildGeneratedConfigUsernameFallback(t *testing.T) { + // Test fallback to OVF UserName when IMDS AdminUsername is empty + meta := &instanceMetadata{ + Compute: instanceComputeMetadata{ + OSProfile: instanceOSProfile{ + AdminUsername: "", + }, + }, + } + prov := &linuxProvisioningConfigurationSet{ + UserName: "ovf-user", + } + + cfg, err := buildGeneratedConfig(testLogger(t), meta, prov) + if err != nil { + t.Fatalf("buildGeneratedConfig() err = %v", err) + } + if cfg.Passwd.Users[0].Name != "ovf-user" { + t.Fatalf("expected OVF username 'ovf-user' as fallback, got %s", cfg.Passwd.Users[0].Name) + } +} + +func TestBuildGeneratedConfigNoPassword(t *testing.T) { + meta := &instanceMetadata{ + Compute: instanceComputeMetadata{ + OSProfile: instanceOSProfile{ + AdminUsername: "testuser", + }, + }, + } + prov := &linuxProvisioningConfigurationSet{} + + cfg, err := buildGeneratedConfig(testLogger(t), meta, prov) + if err != nil { + t.Fatalf("buildGeneratedConfig() err = %v", err) + } + if cfg.Passwd.Users[0].PasswordHash != nil { + t.Fatalf("expected nil password hash when no password provided, got %v", *cfg.Passwd.Users[0].PasswordHash) + } +} + +func TestCollectSSHPublicKeysDedup(t *testing.T) { + meta := &instanceMetadata{ + Compute: instanceComputeMetadata{ + PublicKeys: []instancePublicKey{ + {KeyData: "ssh-rsa AAAA"}, + {KeyData: "ssh-rsa AAAA"}, + }, + }, + } + prov := &linuxProvisioningConfigurationSet{ + SSH: sshSection{ + PublicKeys: []sshPublicKey{ + {Value: "ssh-rsa BBBB"}, + {Value: "ssh-rsa AAAA"}, + }, + }, + } + keys := collectSSHPublicKeys(meta, prov) + if len(keys) != 2 { + t.Fatalf("expected 2 unique keys, got %d", len(keys)) + } +} + +func TestPasswordAuthDisabledParsing(t *testing.T) { + trueCases := []string{"true", "TRUE", "1", " yes ", "YES"} + for _, tc := range trueCases { + prov := linuxProvisioningConfigurationSet{DisableSshPasswordAuthentication: tc} + if !prov.passwordAuthDisabled() { + t.Fatalf("expected %q to disable password auth", tc) + } + } + falseCases := []string{"false", "0", "no", "", "NO", "False"} + for _, tc := range falseCases { + prov := linuxProvisioningConfigurationSet{DisableSshPasswordAuthentication: tc} + if prov.passwordAuthDisabled() { + t.Fatalf("expected %q to allow password auth", tc) + } + } +} + +func TestHashPassword(t *testing.T) { + password := "testpassword123" + hash, err := HashPassword(password) + if err != nil { + t.Fatalf("HashPassword() err = %v", err) + } + + // Verify hash format + if !strings.HasPrefix(hash, "$6$") { + t.Fatalf("expected SHA-512 hash prefix $6$, got %s", hash) + } + + // Verify hash has expected structure: $6$$ + parts := strings.Split(hash, "$") + if len(parts) != 4 { + t.Fatalf("expected 4 parts in hash, got %d: %s", len(parts), hash) + } + if parts[1] != "6" { + t.Fatalf("expected algorithm identifier '6', got %s", parts[1]) + } + if len(parts[2]) != 16 { + t.Fatalf("expected 16 character salt, got %d: %s", len(parts[2]), parts[2]) + } + if len(parts[3]) != 86 { + t.Fatalf("expected 86 character hash, got %d: %s", len(parts[3]), parts[3]) + } +} + +func TestIsPasswordHashed(t *testing.T) { + tests := []struct { + password string + expected bool + }{ + {"$6$salt$hash", true}, + {"$5$salt$hash", true}, + {"$y$salt$hash", true}, + {"$2a$10$hash", true}, + {"$2b$10$hash", true}, + {"$2y$10$hash", true}, + {"$1$salt$hash", true}, + {"plaintext", false}, + {"$invalid", false}, + {"", false}, + } + + for _, tt := range tests { + result := IsPasswordHashed(tt.password) + if result != tt.expected { + t.Errorf("IsPasswordHashed(%q) = %v, expected %v", tt.password, result, tt.expected) + } + } +} + +func TestGenerateCloudConfigSuccess(t *testing.T) { + t.Cleanup(func() { + fetchInstanceMetadataFunc = fetchInstanceMetadata + readOvfEnvironmentFunc = readOvfEnvironment + }) + + fetchInstanceMetadataFunc = func(f *resource.Fetcher) (*instanceMetadata, error) { + return &instanceMetadata{ + Compute: instanceComputeMetadata{ + OSProfile: instanceOSProfile{AdminUsername: "imds-user"}, + PublicKeys: []instancePublicKey{ + {KeyData: "ssh-rsa AAAA"}, + }, + }, + }, nil + } + ovf := []byte(` + + ovf-user + password + true + ZWNobyBoZWxsbwo= + + + ssh-ed25519 BBBB + + + +`) + readOvfEnvironmentFunc = func(f *resource.Fetcher, _ []string) ([]byte, error) { + return ovf, nil + } + + fetcher := newStubFetcher() + cfg, err := generateCloudConfig(&fetcher.Fetcher) + if err != nil { + t.Fatalf("generateCloudConfig() err = %v", err) + } + if len(cfg.Passwd.Users) != 1 { + t.Fatalf("expected 1 user, got %d", len(cfg.Passwd.Users)) + } + if cfg.Passwd.Users[0].Name != "imds-user" { + t.Fatalf("expected username imds-user, got %s", cfg.Passwd.Users[0].Name) + } + if len(cfg.Passwd.Users[0].SSHAuthorizedKeys) != 2 { + t.Fatalf("expected merged ssh keys, got %d", len(cfg.Passwd.Users[0].SSHAuthorizedKeys)) + } + if len(cfg.Storage.Files) != 3 { + t.Fatalf("expected 3 generated files, got %d", len(cfg.Storage.Files)) + } + customFile := fileByPath(t, cfg.Storage.Files, "/var/lib/waagent/CustomData") + if customFile.Contents.Source == nil { + t.Fatalf("expected custom data file to have contents") + } + if got := dataURLContents(t, *customFile.Contents.Source); got != "echo hello\n" { + t.Fatalf("unexpected custom data contents: %q", got) + } +} + +func TestGenerateCloudConfigNeedNet(t *testing.T) { + t.Cleanup(func() { + fetchInstanceMetadataFunc = fetchInstanceMetadata + readOvfEnvironmentFunc = readOvfEnvironment + }) + wantErr := resource.ErrNeedNet + fetchInstanceMetadataFunc = func(f *resource.Fetcher) (*instanceMetadata, error) { + return nil, wantErr + } + readOvfEnvironmentFunc = func(f *resource.Fetcher, _ []string) ([]byte, error) { + return nil, fmt.Errorf("should not be called") + } + + fetcher := newStubFetcher() + _, err := generateCloudConfig(&fetcher.Fetcher) + if err == nil || err.Error() != fmt.Sprintf("fetching instance metadata: %v", wantErr) { + t.Fatalf("expected wrapped ErrNeedNet, got %v", err) + } +} + +func TestGenerateCloudConfigFallbackToProvisioning(t *testing.T) { + t.Cleanup(func() { + fetchInstanceMetadataFunc = fetchInstanceMetadata + readOvfEnvironmentFunc = readOvfEnvironment + }) + + fetchInstanceMetadataFunc = func(f *resource.Fetcher) (*instanceMetadata, error) { + return nil, fmt.Errorf("imds unavailable") + } + ovf := []byte(` + + ovf-only + + + ssh-rsa OOOO + + + +`) + readOvfEnvironmentFunc = func(f *resource.Fetcher, _ []string) ([]byte, error) { + return ovf, nil + } + + fetcher := newStubFetcher() + cfg, err := generateCloudConfig(&fetcher.Fetcher) + if err != nil { + t.Fatalf("expected success, got %v", err) + } + if cfg.Passwd.Users[0].Name != "ovf-only" { + t.Fatalf("expected username from provisioning data, got %s", cfg.Passwd.Users[0].Name) + } +} diff --git a/internal/providers/azure/password.go b/internal/providers/azure/password.go new file mode 100644 index 000000000..9acc45217 --- /dev/null +++ b/internal/providers/azure/password.go @@ -0,0 +1,84 @@ +// Copyright 2015 CoreOS, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package azure + +import ( + "crypto/rand" + "encoding/base64" + "strings" + + "github.com/GehirnInc/crypt/sha512_crypt" +) + +// HashPassword hashes a plaintext password using SHA-512 crypt. +// Returns a string in the format $6$$. +func HashPassword(password string) (string, error) { + salt, err := generateSalt(16) + if err != nil { + return "", err + } + + crypt := sha512_crypt.New() + return crypt.Generate([]byte(password), []byte("$6$"+salt)) +} + +// IsPasswordHashed checks if a password string is already hashed. +// It recognizes common hash formats: SHA-512 ($6$), SHA-256 ($5$), +// yescrypt ($y$), bcrypt ($2a$, $2b$, $2y$), and MD5 ($1$). +func IsPasswordHashed(password string) bool { + if password == "" { + return false + } + + // Check for common password hash prefixes + hashPrefixes := []string{ + "$6$", // SHA-512 + "$5$", // SHA-256 + "$y$", // yescrypt + "$2a$", // bcrypt + "$2b$", // bcrypt + "$2y$", // bcrypt + "$1$", // MD5 + } + + for _, prefix := range hashPrefixes { + if strings.HasPrefix(password, prefix) { + // Verify it has the expected structure (at least prefix + something) + remaining := password[len(prefix):] + if len(remaining) > 0 && strings.Contains(remaining, "$") { + return true + } + } + } + + return false +} + +// generateSalt generates a random salt of the specified length. +func generateSalt(length int) (string, error) { + bytes := make([]byte, length) + if _, err := rand.Read(bytes); err != nil { + return "", err + } + // Use base64 encoding and trim to desired length + salt := base64.StdEncoding.EncodeToString(bytes) + // Remove any characters that might cause issues in crypt salt + salt = strings.ReplaceAll(salt, "+", ".") + salt = strings.ReplaceAll(salt, "=", "") + if len(salt) > length { + salt = salt[:length] + } + return salt, nil +} diff --git a/vendor/github.com/GehirnInc/crypt/.travis.yml b/vendor/github.com/GehirnInc/crypt/.travis.yml new file mode 100644 index 000000000..6a63bc8e8 --- /dev/null +++ b/vendor/github.com/GehirnInc/crypt/.travis.yml @@ -0,0 +1,7 @@ +language: go +go: + - 1.6.x + - 1.7.x + - master +script: + - go test -v -race ./... diff --git a/vendor/github.com/GehirnInc/crypt/AUTHORS.md b/vendor/github.com/GehirnInc/crypt/AUTHORS.md new file mode 100644 index 000000000..4490cf22b --- /dev/null +++ b/vendor/github.com/GehirnInc/crypt/AUTHORS.md @@ -0,0 +1,8 @@ +### Initial author + +[Jeramey Crawford](https://github.com/jeramey) + +### Other authors + +- [Jonas mg](https://github.com/kless) +- [Kohei YOSHIDA](https://github.com/yosida95) diff --git a/vendor/github.com/GehirnInc/crypt/LICENSE b/vendor/github.com/GehirnInc/crypt/LICENSE new file mode 100644 index 000000000..7048fecec --- /dev/null +++ b/vendor/github.com/GehirnInc/crypt/LICENSE @@ -0,0 +1,26 @@ +Copyright (c) 2012, Jeramey Crawford +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/GehirnInc/crypt/README.rst b/vendor/github.com/GehirnInc/crypt/README.rst new file mode 100644 index 000000000..0608624fc --- /dev/null +++ b/vendor/github.com/GehirnInc/crypt/README.rst @@ -0,0 +1,61 @@ +.. image:: https://travis-ci.org/GehirnInc/crypt.svg?branch=master + :target: https://travis-ci.org/GehirnInc/crypt + +crypt - A password hashing library for Go +========================================= +crypt provides pure golang implementations of UNIX's crypt(3). + +The goal of crypt is to bring a library of many common and popular password +hashing algorithms to Go and to provide a simple and consistent interface to +each of them. As every hashing method is implemented in pure Go, this library +should be as portable as Go itself. + +All hashing methods come with a test suite which verifies their operation +against itself as well as the output of other password hashing implementations +to ensure compatibility with them. + +I hope you find this library to be useful and easy to use! + +Install +------- + +To install crypt, use the *go get* command. + +.. code-block:: sh + + go get github.com/GehirnInc/crypt + + +Usage +----- + +.. code-block:: go + + package main + + import ( + "fmt" + + "github.com/GehirnInc/crypt" + _ "github.com/GehirnInc/crypt/sha256_crypt" + ) + + func main() { + crypt := crypt.SHA256.New() + ret, _ := crypt.Generate([]byte("secret"), []byte("$5$salt")) + fmt.Println(ret) + + err := crypt.Verify(ret, []byte("secret")) + fmt.Println(err) + + // Output: + // $5$salt$kpa26zwgX83BPSR8d7w93OIXbFt/d3UOTZaAu5vsTM6 + // + } + +Documentation +------------- + +The documentation is available on GoDoc_. + +.. _GoDoc: https://godoc.org/github.com/GehirnInc/crypt diff --git a/vendor/github.com/GehirnInc/crypt/common/base64.go b/vendor/github.com/GehirnInc/crypt/common/base64.go new file mode 100644 index 000000000..ee5240e10 --- /dev/null +++ b/vendor/github.com/GehirnInc/crypt/common/base64.go @@ -0,0 +1,59 @@ +// (C) Copyright 2012, Jeramey Crawford . All +// rights reserved. Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package common + +const ( + alphabet = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" +) + +// Base64_24Bit is a variant of Base64 encoding, commonly used with password +// hashing algorithms to encode the result of their checksum output. +// +// The algorithm operates on up to 3 bytes at a time, encoding the following +// 6-bit sequences into up to 4 hash64 ASCII bytes. +// +// 1. Bottom 6 bits of the first byte +// 2. Top 2 bits of the first byte, and bottom 4 bits of the second byte. +// 3. Top 4 bits of the second byte, and bottom 2 bits of the third byte. +// 4. Top 6 bits of the third byte. +// +// This encoding method does not emit padding bytes as Base64 does. +func Base64_24Bit(src []byte) []byte { + if len(src) == 0 { + return []byte{} // TODO: return nil + } + + dstlen := (len(src)*8 + 5) / 6 + dst := make([]byte, dstlen) + + di, si := 0, 0 + n := len(src) / 3 * 3 + for si < n { + val := uint(src[si+2])<<16 | uint(src[si+1])<<8 | uint(src[si]) + dst[di+0] = alphabet[val&0x3f] + dst[di+1] = alphabet[val>>6&0x3f] + dst[di+2] = alphabet[val>>12&0x3f] + dst[di+3] = alphabet[val>>18] + di += 4 + si += 3 + } + + rem := len(src) - si + if rem == 0 { + return dst + } + + val := uint(src[si+0]) + if rem == 2 { + val |= uint(src[si+1]) << 8 + } + + dst[di+0] = alphabet[val&0x3f] + dst[di+1] = alphabet[val>>6&0x3f] + if rem == 2 { + dst[di+2] = alphabet[val>>12] + } + return dst +} diff --git a/vendor/github.com/GehirnInc/crypt/common/doc.go b/vendor/github.com/GehirnInc/crypt/common/doc.go new file mode 100644 index 000000000..8ba84e960 --- /dev/null +++ b/vendor/github.com/GehirnInc/crypt/common/doc.go @@ -0,0 +1,10 @@ +// (C) Copyright 2012, Jeramey Crawford . All +// rights reserved. Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package common contains routines used by multiple password hashing +// algorithms. +// +// Generally, you will never import this package directly. Many of the +// *_crypt packages will import this package if they require it. +package common diff --git a/vendor/github.com/GehirnInc/crypt/common/salt.go b/vendor/github.com/GehirnInc/crypt/common/salt.go new file mode 100644 index 000000000..54372be0f --- /dev/null +++ b/vendor/github.com/GehirnInc/crypt/common/salt.go @@ -0,0 +1,148 @@ +// (C) Copyright 2012, Jeramey Crawford . All +// rights reserved. Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package common + +import ( + "bytes" + "crypto/rand" + "errors" + "strconv" +) + +var ( + ErrSaltPrefix = errors.New("invalid magic prefix") + ErrSaltFormat = errors.New("invalid salt format") + ErrSaltRounds = errors.New("invalid rounds") +) + +const ( + roundsPrefix = "rounds=" +) + +// Salt represents a salt. +type Salt struct { + MagicPrefix []byte + + SaltLenMin int + SaltLenMax int + + RoundsMin int + RoundsMax int + RoundsDefault int +} + +// Generate generates a random salt of a given length. +// +// The length is set thus: +// +// length > SaltLenMax: length = SaltLenMax +// length < SaltLenMin: length = SaltLenMin +func (s *Salt) Generate(length int) []byte { + if length > s.SaltLenMax { + length = s.SaltLenMax + } else if length < s.SaltLenMin { + length = s.SaltLenMin + } + + saltLen := (length * 6 / 8) + if (length*6)%8 != 0 { + saltLen += 1 + } + salt := make([]byte, saltLen) + rand.Read(salt) + + out := make([]byte, len(s.MagicPrefix)+length) + copy(out, s.MagicPrefix) + copy(out[len(s.MagicPrefix):], Base64_24Bit(salt)) + return out +} + +// GenerateWRounds creates a random salt with the random bytes being of the +// length provided, and the rounds parameter set as specified. +// +// The parameters are set thus: +// +// length > SaltLenMax: length = SaltLenMax +// length < SaltLenMin: length = SaltLenMin +// +// rounds < 0: rounds = RoundsDefault +// rounds < RoundsMin: rounds = RoundsMin +// rounds > RoundsMax: rounds = RoundsMax +// +// If rounds is equal to RoundsDefault, then the "rounds=" part of the salt is +// removed. +func (s *Salt) GenerateWRounds(length, rounds int) []byte { + if length > s.SaltLenMax { + length = s.SaltLenMax + } else if length < s.SaltLenMin { + length = s.SaltLenMin + } + if rounds < 0 { + rounds = s.RoundsDefault + } else if rounds < s.RoundsMin { + rounds = s.RoundsMin + } else if rounds > s.RoundsMax { + rounds = s.RoundsMax + } + + saltLen := (length * 6 / 8) + if (length*6)%8 != 0 { + saltLen += 1 + } + salt := make([]byte, saltLen) + rand.Read(salt) + + roundsText := "" + if rounds != s.RoundsDefault { + roundsText = roundsPrefix + strconv.Itoa(rounds) + "$" + } + + out := make([]byte, len(s.MagicPrefix)+len(roundsText)+length) + copy(out, s.MagicPrefix) + copy(out[len(s.MagicPrefix):], []byte(roundsText)) + copy(out[len(s.MagicPrefix)+len(roundsText):], Base64_24Bit(salt)) + return out +} + +func (s *Salt) Decode(raw []byte) (salt []byte, rounds int, isRoundsDef bool, rest []byte, err error) { + tokens := bytes.SplitN(raw, []byte{'$'}, 4) + if len(tokens) < 3 { + err = ErrSaltFormat + return + } + if !bytes.HasPrefix(raw, s.MagicPrefix) { + err = ErrSaltPrefix + return + } + + if bytes.HasPrefix(tokens[2], []byte(roundsPrefix)) { + if len(tokens) < 4 { + err = ErrSaltFormat + return + } + salt = tokens[3] + + rounds, err = strconv.Atoi(string(tokens[2][len(roundsPrefix):])) + if err != nil { + err = ErrSaltRounds + return + } + if rounds < s.RoundsMin { + rounds = s.RoundsMin + } + if rounds > s.RoundsMax { + rounds = s.RoundsMax + } + isRoundsDef = true + } else { + salt = tokens[2] + rounds = s.RoundsDefault + } + if len(salt) > s.SaltLenMax { + salt = salt[0:s.SaltLenMax] + } + + return +} diff --git a/vendor/github.com/GehirnInc/crypt/crypt.go b/vendor/github.com/GehirnInc/crypt/crypt.go new file mode 100644 index 000000000..1b4151f38 --- /dev/null +++ b/vendor/github.com/GehirnInc/crypt/crypt.go @@ -0,0 +1,121 @@ +// (C) Copyright 2013, Jonas mg. All rights reserved. +// Use of this source code is governed by a BSD-style license +// that can be found in the LICENSE file. + +// Package crypt provides interface for password crypt functions and collects +// common constants. +package crypt + +import ( + "errors" + "strings" + + "github.com/GehirnInc/crypt/common" +) + +var ErrKeyMismatch = errors.New("hashed value is not the hash of the given password") + +// Crypter is the common interface implemented by all crypt functions. +type Crypter interface { + // Generate performs the hashing algorithm, returning a full hash suitable + // for storage and later password verification. + // + // If the salt is empty, a randomly-generated salt will be generated with a + // length of SaltLenMax and number RoundsDefault of rounds. + // + // Any error only can be got when the salt argument is not empty. + Generate(key, salt []byte) (string, error) + + // Verify compares a hashed key with its possible key equivalent. + // Returns nil on success, or an error on failure; if the hashed key is + // diffrent, the error is "ErrKeyMismatch". + Verify(hashedKey string, key []byte) error + + // Cost returns the hashing cost (in rounds) used to create the given hashed + // key. + // + // When, in the future, the hashing cost of a key needs to be increased in + // order to adjust for greater computational power, this function allows one + // to establish which keys need to be updated. + // + // The algorithms based in MD5-crypt use a fixed value of rounds. + Cost(hashedKey string) (int, error) + + // SetSalt sets a different salt. It is used to easily create derivated + // algorithms, i.e. "apr1_crypt" from "md5_crypt". + SetSalt(salt common.Salt) +} + +// Crypt identifies a crypt function that is implemented in another package. +type Crypt uint + +const ( + APR1 Crypt = 1 + iota // import github.com/GehirnInc/crypt/apr1_crypt + MD5 // import github.com/GehirnInc/crypt/md5_crypt + SHA256 // import github.com/GehirnInc/crypt/sha256_crypt + SHA512 // import github.com/GehirnInc/crypt/sha512_crypt + maxCrypt +) + +var crypts = make([]func() Crypter, maxCrypt) + +// New returns new Crypter making the Crypt c. +// New panics if the Crypt c is unavailable. +func (c Crypt) New() Crypter { + if c > 0 && c < maxCrypt { + f := crypts[c] + if f != nil { + return f() + } + } + panic("crypt: requested crypt function is unavailable") +} + +// Available reports whether the Crypt c is available. +func (c Crypt) Available() bool { + return c > 0 && c < maxCrypt && crypts[c] != nil +} + +var cryptPrefixes = make([]string, maxCrypt) + +// RegisterCrypt registers a function that returns a new instance of the given +// crypt function. This is intended to be called from the init function in +// packages that implement crypt functions. +func RegisterCrypt(c Crypt, f func() Crypter, prefix string) { + if c >= maxCrypt { + panic("crypt: RegisterHash of unknown crypt function") + } + crypts[c] = f + cryptPrefixes[c] = prefix +} + +// New returns a new crypter. +func New(c Crypt) Crypter { + return c.New() +} + +// IsHashSupported returns true if hashedKey has a supported prefix. +// NewFromHash will not panic for this hashedKey +func IsHashSupported(hashedKey string) bool { + for i := range cryptPrefixes { + prefix := cryptPrefixes[i] + if crypts[i] != nil && strings.HasPrefix(hashedKey, prefix) { + return true + } + } + + return false +} + +// NewFromHash returns a new Crypter using the prefix in the given hashed key. +func NewFromHash(hashedKey string) Crypter { + for i := range cryptPrefixes { + prefix := cryptPrefixes[i] + if crypts[i] != nil && strings.HasPrefix(hashedKey, prefix) { + crypt := Crypt(uint(i)) + return crypt.New() + } + } + + panic("crypt: unknown crypt function") +} diff --git a/vendor/github.com/GehirnInc/crypt/internal/utils.go b/vendor/github.com/GehirnInc/crypt/internal/utils.go new file mode 100644 index 000000000..2d36e86ab --- /dev/null +++ b/vendor/github.com/GehirnInc/crypt/internal/utils.go @@ -0,0 +1,41 @@ +// Copyright (c) 2015 Kohei YOSHIDA. All rights reserved. +// This software is licensed under the 3-Clause BSD License +// that can be found in LICENSE file. +package internal + +const ( + cleanBytesLen = 64 +) + +var ( + cleanBytes = make([]byte, cleanBytesLen) +) + +func CleanSensitiveData(b []byte) { + l := len(b) + + for ; l > cleanBytesLen; l -= cleanBytesLen { + copy(b[l-cleanBytesLen:l], cleanBytes) + } + + if l > 0 { + copy(b[0:l], cleanBytes[0:l]) + } +} + +func RepeatByteSequence(input []byte, length int) []byte { + var ( + sequence = make([]byte, length) + unit = len(input) + ) + + j := length / unit * unit + for i := 0; i < j; i += unit { + copy(sequence[i:length], input) + } + if j < length { + copy(sequence[j:length], input[0:length-j]) + } + + return sequence +} diff --git a/vendor/github.com/GehirnInc/crypt/sha512_crypt/sha512_crypt.go b/vendor/github.com/GehirnInc/crypt/sha512_crypt/sha512_crypt.go new file mode 100644 index 000000000..1037d73ca --- /dev/null +++ b/vendor/github.com/GehirnInc/crypt/sha512_crypt/sha512_crypt.go @@ -0,0 +1,188 @@ +// (C) Copyright 2012, Jeramey Crawford . All +// rights reserved. Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package sha512_crypt implements Ulrich Drepper's SHA512-crypt password +// hashing algorithm. +// +// The specification for this algorithm can be found here: +// http://www.akkadia.org/drepper/SHA-crypt.txt +package sha512_crypt + +import ( + "bytes" + "crypto/sha512" + "crypto/subtle" + "strconv" + + "github.com/GehirnInc/crypt" + "github.com/GehirnInc/crypt/common" + "github.com/GehirnInc/crypt/internal" +) + +func init() { + crypt.RegisterCrypt(crypt.SHA512, New, MagicPrefix) +} + +const ( + MagicPrefix = "$6$" + SaltLenMin = 1 + SaltLenMax = 16 + RoundsMin = 1000 + RoundsMax = 999999999 + RoundsDefault = 5000 +) + +var _rounds = []byte("rounds=") + +type crypter struct{ Salt common.Salt } + +// New returns a new crypt.Crypter computing the SHA512-crypt password hashing. +func New() crypt.Crypter { + return &crypter{ + common.Salt{ + MagicPrefix: []byte(MagicPrefix), + SaltLenMin: SaltLenMin, + SaltLenMax: SaltLenMax, + RoundsDefault: RoundsDefault, + RoundsMin: RoundsMin, + RoundsMax: RoundsMax, + }, + } +} + +func (c *crypter) Generate(key, salt []byte) (string, error) { + if len(salt) == 0 { + salt = c.Salt.GenerateWRounds(SaltLenMax, RoundsDefault) + } + salt, rounds, isRoundsDef, _, err := c.Salt.Decode(salt) + if err != nil { + return "", err + } + + keyLen := len(key) + saltLen := len(salt) + h := sha512.New() + + // compute sumB + // step 4-8 + h.Write(key) + h.Write(salt) + h.Write(key) + sumB := h.Sum(nil) + + // Compute sumA + // step 1-3, 9-12 + h.Reset() + h.Write(key) + h.Write(salt) + h.Write(internal.RepeatByteSequence(sumB, keyLen)) + for i := keyLen; i > 0; i >>= 1 { + if i%2 == 0 { + h.Write(key) + } else { + h.Write(sumB) + } + } + sumA := h.Sum(nil) + internal.CleanSensitiveData(sumB) + + // Compute seqP + // step 13-16 + h.Reset() + for i := 0; i < keyLen; i++ { + h.Write(key) + } + seqP := internal.RepeatByteSequence(h.Sum(nil), keyLen) + + // Compute seqS + // step 17-20 + h.Reset() + for i := 0; i < 16+int(sumA[0]); i++ { + h.Write(salt) + } + seqS := internal.RepeatByteSequence(h.Sum(nil), saltLen) + + // step 21 + for i := 0; i < rounds; i++ { + h.Reset() + + if i&1 != 0 { + h.Write(seqP) + } else { + h.Write(sumA) + } + if i%3 != 0 { + h.Write(seqS) + } + if i%7 != 0 { + h.Write(seqP) + } + if i&1 != 0 { + h.Write(sumA) + } else { + h.Write(seqP) + } + copy(sumA, h.Sum(nil)) + } + internal.CleanSensitiveData(seqP) + internal.CleanSensitiveData(seqS) + + // make output + buf := bytes.Buffer{} + buf.Grow(len(c.Salt.MagicPrefix) + len(_rounds) + 9 + 1 + len(salt) + 1 + 86) + buf.Write(c.Salt.MagicPrefix) + if isRoundsDef { + buf.Write(_rounds) + buf.WriteString(strconv.Itoa(rounds)) + buf.WriteByte('$') + } + buf.Write(salt) + buf.WriteByte('$') + buf.Write(common.Base64_24Bit([]byte{ + sumA[42], sumA[21], sumA[0], + sumA[1], sumA[43], sumA[22], + sumA[23], sumA[2], sumA[44], + sumA[45], sumA[24], sumA[3], + sumA[4], sumA[46], sumA[25], + sumA[26], sumA[5], sumA[47], + sumA[48], sumA[27], sumA[6], + sumA[7], sumA[49], sumA[28], + sumA[29], sumA[8], sumA[50], + sumA[51], sumA[30], sumA[9], + sumA[10], sumA[52], sumA[31], + sumA[32], sumA[11], sumA[53], + sumA[54], sumA[33], sumA[12], + sumA[13], sumA[55], sumA[34], + sumA[35], sumA[14], sumA[56], + sumA[57], sumA[36], sumA[15], + sumA[16], sumA[58], sumA[37], + sumA[38], sumA[17], sumA[59], + sumA[60], sumA[39], sumA[18], + sumA[19], sumA[61], sumA[40], + sumA[41], sumA[20], sumA[62], + sumA[63], + })) + return buf.String(), nil +} + +func (c *crypter) Verify(hashedKey string, key []byte) error { + newHash, err := c.Generate(key, []byte(hashedKey)) + if err != nil { + return err + } + if subtle.ConstantTimeCompare([]byte(newHash), []byte(hashedKey)) != 1 { + return crypt.ErrKeyMismatch + } + return nil +} + +func (c *crypter) Cost(hashedKey string) (int, error) { + _, rounds, _, _, err := c.Salt.Decode([]byte(hashedKey)) + if err != nil { + return 0, err + } + return rounds, nil +} + +func (c *crypter) SetSalt(salt common.Salt) { c.Salt = salt } diff --git a/vendor/modules.txt b/vendor/modules.txt index 136f696c5..defc53fac 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -125,6 +125,10 @@ github.com/AzureAD/microsoft-authentication-library-for-go/apps/managedidentity github.com/AzureAD/microsoft-authentication-library-for-go/apps/public # github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5 ## explicit; go 1.19 +github.com/GehirnInc/crypt +github.com/GehirnInc/crypt/common +github.com/GehirnInc/crypt/internal +github.com/GehirnInc/crypt/sha512_crypt # github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 ## explicit; go 1.24.0 github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp