diff --git a/docs/release-notes.md b/docs/release-notes.md
index 80c9ebe9c..5733ecbab 100644
--- a/docs/release-notes.md
+++ b/docs/release-notes.md
@@ -10,6 +10,7 @@ nav_order: 9
### Features
- The name for custom clevis pins is not validated by Ignition anymore, enabling the use of arbitrary custom pins.
+- Azure images can opt into `--generate-cloud-config=azure`, letting `ignition-fetch.service` synthesize an admin-user config from IMDS metadata and the provisioning media.
### Changes
diff --git a/dracut/30ignition/ignition-generator b/dracut/30ignition/ignition-generator
index 1dff74423..446c5fd15 100755
--- a/dracut/30ignition/ignition-generator
+++ b/dracut/30ignition/ignition-generator
@@ -59,4 +59,24 @@ else
fi
fi
-echo "PLATFORM_ID=$(cmdline_arg ignition.platform.id)" > /run/ignition.env
+platform_id="$(cmdline_arg ignition.platform.id)"
+ignition_args=()
+
+# Check if cloud config generation is requested via kernel cmdline
+# Usage: ignition.config.generate=azure
+generate_config="$(cmdline_arg ignition.config.generate)"
+if [[ -n "${generate_config}" ]]; then
+ # Basic validation to prevent command injection. The value should be a
+ # simple platform identifier.
+ if [[ "${generate_config}" =~ ^[a-zA-Z0-9_-]+$ ]]; then
+ ignition_args+=("--generate-cloud-config=${generate_config}")
+ else
+ # Log to stderr, which is redirected to kmsg
+ echo "ignition-generator: invalid value for ignition.config.generate: ${generate_config}" >&2
+ fi
+fi
+
+{
+ echo "PLATFORM_ID=${platform_id}"
+ echo "IGNITION_ARGS=${ignition_args[*]}"
+} > /run/ignition.env
diff --git a/go.mod b/go.mod
index f8b16f836..ab4fa0fbd 100644
--- a/go.mod
+++ b/go.mod
@@ -9,6 +9,7 @@ require (
cloud.google.com/go/storage v1.57.0
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.0
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.39.3
github.com/aws/aws-sdk-go-v2/credentials v1.18.17
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.10
diff --git a/go.sum b/go.sum
index a22d889f2..9ec1365ed 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.5.0 h1:XkkQbfMyuH2jTSjQjSoihryI8GINRcs4xp8lNawg0FI=
github.com/AzureAD/microsoft-authentication-library-for-go v1.5.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.29.0 h1:UQUsRi8WTzhZntp5313l+CHIAT95ojUI2lpP/ExlZa4=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0/go.mod h1:Cz6ft6Dkn3Et6l2v2a9/RpN7epQ1GtDlO6lj8bEcOvw=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0 h1:owcC2UnmsZycprQ5RfRgjydWhuoxg71LUfyiQdijZuM=
diff --git a/internal/exec/engine.go b/internal/exec/engine.go
index c2e61f7eb..3aea2ffad 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 string
+ 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,31 @@ func (e *Engine) fetchProviderConfig() (types.Config, error) {
return configFetcher.RenderConfig(cfg)
}
+func (e *Engine) fetchGeneratedConfig() (types.Config, error) {
+ if e.GenerateCloudConfig != e.PlatformConfig.Name() {
+ return types.Config{}, fmt.Errorf("cannot generate %q config on %q platform", e.GenerateCloudConfig, 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.GenerateCloudConfig),
+ 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..a10e92073 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 string
+ 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.StringVar(&flags.generateCloudConfig, "generate-cloud-config", "", "generate a platform-specific config instead of fetching (e.g. azure)")
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()))
@@ -104,14 +106,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..c12c47991 100644
--- a/internal/platform/platform.go
+++ b/internal/platform/platform.go
@@ -48,6 +48,8 @@ type Provider struct {
Init func(f *resource.Fetcher) error
Status func(stageName string, f resource.Fetcher, e error) error
DelConfig func(f *resource.Fetcher) error
+ // Generates an azure-specific Ignition config.
+ 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.
@@ -104,6 +106,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..7459ce752 100644
--- a/internal/providers/azure/azure.go
+++ b/internal/providers/azure/azure.go
@@ -18,21 +18,27 @@ package azure
import (
"encoding/base64"
+ "encoding/json"
+ "encoding/xml"
"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"
+ 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 +74,26 @@ 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-01-01&format=json&extended=true",
+ }
)
+var imdsRetryCodes = []int{
+ 404,
+ 410,
+ 429,
+}
+
func init() {
platform.Register(platform.Provider{
- Name: "azure",
- NewFetcher: newFetcher,
- Fetch: fetchConfig,
+ Name: "azure",
+ NewFetcher: newFetcher,
+ Fetch: fetchConfig,
+ GenerateCloudConfig: generateCloudConfig,
})
}
@@ -134,12 +153,7 @@ 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})
+ data, err := f.FetchToBuffer(imdsUserdataURL, resource.FetchOptions{Headers: headers, RetryCodes: imdsRetryCodes})
if err != nil {
return nil, fmt.Errorf("fetching to buffer: %w", err)
}
@@ -200,42 +214,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 +255,291 @@ 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) {
+ meta, err := fetchInstanceMetadata(f)
+ if err != nil {
+ return types.Config{}, fmt.Errorf("fetching instance metadata: %w", err)
+ }
+
+ ovfRaw, err := readOvfEnvironment(f, []string{CDS_FSTYPE_UDF})
+ if err != nil {
+ return types.Config{}, fmt.Errorf("reading provisioning metadata: %w", err)
+ }
+ if len(ovfRaw) == 0 {
+ return types.Config{}, fmt.Errorf("ovf-env.xml was empty")
+ }
+
+ provisioning, err := parseProvisioningConfig(ovfRaw)
+ if err != nil {
+ return types.Config{}, fmt.Errorf("parsing provisioning metadata: %w", err)
+ }
+
+ return buildGeneratedConfig(meta, provisioning)
+}
+
+func fetchInstanceMetadata(f *resource.Fetcher) (*instanceMetadata, error) {
+ headers := make(http.Header)
+ headers.Set("Metadata", "true")
+ data, err := f.FetchToBuffer(imdsInstanceURL, resource.FetchOptions{Headers: headers, RetryCodes: imdsRetryCodes})
+ 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(meta *instanceMetadata, provisioning *linuxProvisioningConfigurationSet) (types.Config, error) {
+ username := strings.TrimSpace(meta.Compute.OSProfile.AdminUsername)
+ if username == "" {
+ username = strings.TrimSpace(provisioning.UserName)
+ }
+ if username == "" {
+ return types.Config{}, fmt.Errorf("unable to determine admin username from metadata or provisioning data")
+ }
+
+ 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/99_wheel_nopasswd", 0440, "%wheel ALL=(ALL) NOPASSWD:ALL\n")
+ passwordSetting := "yes"
+ if passwordAuthDisabled {
+ passwordSetting = "no"
+ }
+ sshConfig := fmt.Sprintf(`# Custom SSHD settings
+PasswordAuthentication %s
+PermitRootLogin no
+AllowUsers %s
+`, passwordSetting, username)
+ sshdFile := newDataFile("/etc/ssh/sshd_config.d/10-custom.conf", 0644, sshConfig)
+
+ return types.Config{
+ Ignition: types.Ignition{
+ Version: types.MaxVersion.String(),
+ },
+ Passwd: types.Passwd{
+ Users: []types.PasswdUser{user},
+ },
+ Storage: types.Storage{
+ Files: []types.File{sudoersFile, sshdFile},
+ },
+ }, nil
+}
+
+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..4605a51f2
--- /dev/null
+++ b/internal/providers/azure/azure_test.go
@@ -0,0 +1,485 @@
+package azure
+
+import (
+ "strings"
+ "testing"
+)
+
+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 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(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(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 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 TestNewDataFile(t *testing.T) {
+ content := "line1\n"
+ file := newDataFile("/tmp/example", 0640, content)
+ if file.Path != "/tmp/example" {
+ t.Fatalf("unexpected path %s", file.Path)
+ }
+ if file.Mode == nil || *file.Mode != 0640 {
+ t.Fatalf("unexpected mode %#v", file.Mode)
+ }
+ if file.Contents.Source == nil || !strings.HasPrefix(*file.Contents.Source, "data:") {
+ t.Fatalf("expected contents to be a data URL, got %v", file.Contents.Source)
+ }
+}
+
+func TestBuildGeneratedConfigErrors(t *testing.T) {
+ meta := &instanceMetadata{}
+ prov := &linuxProvisioningConfigurationSet{}
+ if _, err := buildGeneratedConfig(meta, prov); err == nil {
+ t.Fatalf("expected error when username missing")
+ }
+}
+
+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 TestHashPasswordDifferentSalts(t *testing.T) {
+ password := "testpassword123"
+ hash1, err := HashPassword(password)
+ if err != nil {
+ t.Fatalf("HashPassword() err = %v", err)
+ }
+ hash2, err := HashPassword(password)
+ if err != nil {
+ t.Fatalf("HashPassword() err = %v", err)
+ }
+
+ // Hashes should be different due to random salt
+ if hash1 == hash2 {
+ t.Fatalf("expected different hashes for same password (different salts)")
+ }
+}
+
+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 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(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(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 TestBuildGeneratedConfigWhitespaceUsername(t *testing.T) {
+ // Test that whitespace-only usernames are treated as empty
+ meta := &instanceMetadata{
+ Compute: instanceComputeMetadata{
+ OSProfile: instanceOSProfile{
+ AdminUsername: " ",
+ },
+ },
+ }
+ prov := &linuxProvisioningConfigurationSet{
+ UserName: "ovf-user",
+ }
+
+ cfg, err := buildGeneratedConfig(meta, prov)
+ if err != nil {
+ t.Fatalf("buildGeneratedConfig() err = %v", err)
+ }
+ if cfg.Passwd.Users[0].Name != "ovf-user" {
+ t.Fatalf("expected OVF username when IMDS is whitespace, got %s", cfg.Passwd.Users[0].Name)
+ }
+}
+
+func TestBuildGeneratedConfigEmptySSHKeys(t *testing.T) {
+ meta := &instanceMetadata{
+ Compute: instanceComputeMetadata{
+ OSProfile: instanceOSProfile{
+ AdminUsername: "testuser",
+ },
+ PublicKeys: []instancePublicKey{},
+ },
+ }
+ prov := &linuxProvisioningConfigurationSet{
+ SSH: sshSection{
+ PublicKeys: []sshPublicKey{},
+ },
+ }
+
+ cfg, err := buildGeneratedConfig(meta, prov)
+ if err != nil {
+ t.Fatalf("buildGeneratedConfig() err = %v", err)
+ }
+ if len(cfg.Passwd.Users[0].SSHAuthorizedKeys) != 0 {
+ t.Fatalf("expected 0 SSH keys, got %d", len(cfg.Passwd.Users[0].SSHAuthorizedKeys))
+ }
+}
+
+func TestBuildGeneratedConfigNoPassword(t *testing.T) {
+ meta := &instanceMetadata{
+ Compute: instanceComputeMetadata{
+ OSProfile: instanceOSProfile{
+ AdminUsername: "testuser",
+ },
+ },
+ }
+ prov := &linuxProvisioningConfigurationSet{}
+
+ cfg, err := buildGeneratedConfig(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 TestBuildGeneratedConfigWhitespacePassword(t *testing.T) {
+ meta := &instanceMetadata{
+ Compute: instanceComputeMetadata{
+ OSProfile: instanceOSProfile{
+ AdminUsername: "testuser",
+ },
+ },
+ }
+ prov := &linuxProvisioningConfigurationSet{
+ UserPassword: " ",
+ }
+
+ cfg, err := buildGeneratedConfig(meta, prov)
+ if err != nil {
+ t.Fatalf("buildGeneratedConfig() err = %v", err)
+ }
+
+ if cfg.Passwd.Users[0].PasswordHash != nil {
+ t.Fatalf("expected nil password hash for whitespace password, got %v", *cfg.Passwd.Users[0].PasswordHash)
+ }
+}
+
+func TestParseProvisioningConfigMalformedXML(t *testing.T) {
+ malformed := []byte(`
+
+ testuser
+
+ `)
+
+ _, err := parseProvisioningConfig(malformed)
+ if err == nil {
+ t.Fatalf("expected error for malformed XML")
+ }
+}
+
+func TestParseProvisioningConfigEmptyXML(t *testing.T) {
+ empty := []byte(``)
+
+ _, err := parseProvisioningConfig(empty)
+ if err == nil {
+ t.Fatalf("expected error for empty XML")
+ }
+}
+
+func TestParseProvisioningConfigMinimalXML(t *testing.T) {
+ minimal := []byte(`
+
+
+ minimaluser
+
+`)
+
+ cfg, err := parseProvisioningConfig(minimal)
+ if err != nil {
+ t.Fatalf("parseProvisioningConfig() err = %v", err)
+ }
+ if cfg.UserName != "minimaluser" {
+ t.Fatalf("expected username 'minimaluser', got %s", cfg.UserName)
+ }
+ if cfg.UserPassword != "" {
+ t.Fatalf("expected empty password, got %s", cfg.UserPassword)
+ }
+ if len(cfg.SSH.PublicKeys) != 0 {
+ t.Fatalf("expected 0 SSH keys, got %d", len(cfg.SSH.PublicKeys))
+ }
+}
+
+func TestParseProvisioningConfigEmptySection(t *testing.T) {
+ emptySection := []byte(`
+
+
+
+`)
+
+ cfg, err := parseProvisioningConfig(emptySection)
+ if err != nil {
+ t.Fatalf("parseProvisioningConfig() err = %v", err)
+ }
+ if cfg.UserName != "" {
+ t.Fatalf("expected empty username, got %s", cfg.UserName)
+ }
+}
+
+func TestCollectSSHPublicKeysNilInputs(t *testing.T) {
+ keys := collectSSHPublicKeys(nil, &linuxProvisioningConfigurationSet{
+ SSH: sshSection{
+ PublicKeys: []sshPublicKey{{Value: "ssh-rsa AAAA"}},
+ },
+ })
+ if len(keys) != 1 {
+ t.Fatalf("expected 1 key with nil metadata, got %d", len(keys))
+ }
+
+ keys = collectSSHPublicKeys(&instanceMetadata{
+ Compute: instanceComputeMetadata{
+ PublicKeys: []instancePublicKey{{KeyData: "ssh-rsa BBBB"}},
+ },
+ }, nil)
+ if len(keys) != 1 {
+ t.Fatalf("expected 1 key with nil provisioning, got %d", len(keys))
+ }
+
+ keys = collectSSHPublicKeys(nil, nil)
+ if len(keys) != 0 {
+ t.Fatalf("expected 0 keys with both nil, got %d", len(keys))
+ }
+}
+
+func TestCollectSSHPublicKeysWhitespaceOnly(t *testing.T) {
+ meta := &instanceMetadata{
+ Compute: instanceComputeMetadata{
+ PublicKeys: []instancePublicKey{
+ {KeyData: " "},
+ {KeyData: "ssh-rsa AAAA"},
+ {KeyData: "\t\n"},
+ },
+ },
+ }
+ prov := &linuxProvisioningConfigurationSet{
+ SSH: sshSection{
+ PublicKeys: []sshPublicKey{
+ {Value: ""},
+ {Value: "ssh-rsa BBBB"},
+ },
+ },
+ }
+
+ keys := collectSSHPublicKeys(meta, prov)
+ if len(keys) != 2 {
+ t.Fatalf("expected 2 non-empty keys, got %d", len(keys))
+ }
+}
diff --git a/internal/providers/azure/crypt.go b/internal/providers/azure/crypt.go
new file mode 100644
index 000000000..8b3d904a6
--- /dev/null
+++ b/internal/providers/azure/crypt.go
@@ -0,0 +1,46 @@
+// 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 (
+ "strings"
+
+ "github.com/GehirnInc/crypt"
+ _ "github.com/GehirnInc/crypt/sha512_crypt" // Register SHA-512 crypt
+)
+
+// HashPassword hashes a plaintext password using SHA-512 crypt algorithm.
+// Returns a string suitable for /etc/shadow in the format $6$$
+func HashPassword(password string) (string, error) {
+ crypter := crypt.SHA512.New()
+ return crypter.Generate([]byte(password), nil)
+}
+
+// IsPasswordHashed checks if a password string appears to already be hashed
+// (starts with a known crypt prefix like $6$, $5$, $y$, etc.)
+func IsPasswordHashed(password string) bool {
+ hashPrefixes := []string{
+ "$6$", // SHA-512
+ "$5$", // SHA-256
+ "$y$", // yescrypt
+ "$2a$", "$2b$", "$2y$", // bcrypt variants
+ "$1$", // MD5 (deprecated)
+ }
+ for _, prefix := range hashPrefixes {
+ if strings.HasPrefix(password, prefix) {
+ return true
+ }
+ }
+ return false
+}
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 8c5df90d2..b2f65268e 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -123,6 +123,12 @@ 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/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.29.0
## explicit; go 1.23.0
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp