Windows パスワード生成の自動化


Compute Engine プロジェクトへの書き込みアクセスを持つユーザーは、gcloud compute reset-windows-password コマンドを使用して、安全に Windows インスタンスのアカウントのパスワードを取得できます。

これは、コマンドがユーザー名と RSA 公開鍵をインスタンスに送信することで行われます。インスタンス上で実行されているエージェントは、次のいずれかを行います。

  • 送信されたユーザー名のアカウントをインスタンス上で作成し、ランダムなパスワードを生成します。
  • アカウントがすでに存在する場合は、パスワードをランダムな値にリセットします。

インスタンスで実行されているエージェントは、受信した公開鍵でパスワードを暗号化し、クライアントに送り返します。パスワードは、対応する秘密鍵で復号されます。

このセクションでは、プロセスの仕組みと、これらのステップをプログラムにより複製するスクリプトの例を説明します。これらのステップを手動で行う場合は、手動での手順をご覧ください。

準備

  • Windows インスタンスを作成します。
  • まだ設定していない場合は、認証を設定します。認証とは、Google Cloud サービスと API にアクセスするために ID を確認するプロセスです。ローカル開発環境からコードまたはサンプルを実行するには、次のいずれかのオプションを選択して Compute Engine に対する認証を行います。

    Select the tab for how you plan to use the samples on this page:

    Console

    When you use the Google Cloud console to access Google Cloud services and APIs, you don't need to set up authentication.

    gcloud

    1. Install the Google Cloud CLI, then initialize it by running the following command:

      gcloud init
    2. Set a default region and zone.
    3. REST

      このページの REST API サンプルをローカル開発環境で使用するには、gcloud CLI に指定した認証情報を使用します。

        Install the Google Cloud CLI, then initialize it by running the following command:

        gcloud init

      詳細については、Google Cloud 認証ドキュメントの REST を使用して認証するをご覧ください。

自動パスワード生成

Go


//  Copyright 2018 Google Inc. All Rights Reserved.
//
//  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 main

import (
	"context"
	"crypto/rand"
	"crypto/rsa"
	"crypto/sha1"
	"encoding/base64"
	"encoding/binary"
	"encoding/json"
	"errors"
	"flag"
	"fmt"
	"log"
	"strings"
	"time"

	daisyCompute "github.com/GoogleCloudPlatform/compute-image-tools/daisy/compute"
	"google.golang.org/api/compute/v1"
)

var (
	instance = flag.String("instance", "", "instance to reset password on")
	zone     = flag.String("zone", "", "zone instance is in")
	project  = flag.String("project", "", "project instance is in")
	user     = flag.String("user", "", "user to reset password for")
)

func getInstanceMetadata(client daisyCompute.Client, i, z, p string) (*compute.Metadata, error) {
	ins, err := client.GetInstance(p, z, i)
	if err != nil {
		return nil, fmt.Errorf("error getting instance: %v", err)
	}

	return ins.Metadata, nil
}

type windowsKeyJSON struct {
	ExpireOn string
	Exponent string
	Modulus  string
	UserName string
}

func generateKey(priv *rsa.PublicKey, u string) (*windowsKeyJSON, error) {
	bs := make([]byte, 4)
	binary.BigEndian.PutUint32(bs, uint32(priv.E))

	return &windowsKeyJSON{
		ExpireOn: time.Now().Add(5 * time.Minute).Format(time.RFC3339),
		// This is different than what the other tools produce,
		// AQAB vs AQABAA==, both are decoded as 65537.
		Exponent: base64.StdEncoding.EncodeToString(bs),
		Modulus:  base64.StdEncoding.EncodeToString(priv.N.Bytes()),
		UserName: u,
	}, nil
}

type credsJSON struct {
	ErrorMessage      string `json:"errorMessage,omitempty"`
	EncryptedPassword string `json:"encryptedPassword,omitempty"`
	Modulus           string `json:"modulus,omitempty"`
}

func getEncryptedPassword(client daisyCompute.Client, i, z, p, mod string) (string, error) {
	out, err := client.GetSerialPortOutput(p, z, i, 4, 0)
	if err != nil {
		return "", err
	}

	for _, line := range strings.Split(out.Contents, "\n") {
		var creds credsJSON
		if err := json.Unmarshal([]byte(line), &creds); err != nil {
			continue
		}
		if creds.Modulus == mod {
			if creds.ErrorMessage != "" {
				return "", fmt.Errorf("error from agent: %s", creds.ErrorMessage)
			}
			return creds.EncryptedPassword, nil
		}
	}
	return "", errors.New("password not found in serial output")
}

func decryptPassword(priv *rsa.PrivateKey, ep string) (string, error) {
	bp, err := base64.StdEncoding.DecodeString(ep)
	if err != nil {
		return "", fmt.Errorf("error decoding password: %v", err)
	}
	pwd, err := rsa.DecryptOAEP(sha1.New(), rand.Reader, priv, bp, nil)
	if err != nil {
		return "", fmt.Errorf("error decrypting password: %v", err)
	}
	return string(pwd), nil
}

func resetPassword(client daisyCompute.Client, i, z, p, u string) (string, error) {
	md, err := getInstanceMetadata(client, *instance, *zone, *project)
	if err != nil {
		return "", fmt.Errorf("error getting instance metadata: %v", err)
	}

	fmt.Println("Generating public/private key pair")
	key, err := rsa.GenerateKey(rand.Reader, 2048)
	if err != nil {
		return "", err
	}

	winKey, err := generateKey(&key.PublicKey, u)
	if err != nil {
		return "", err
	}

	data, err := json.Marshal(winKey)
	if err != nil {
		return "", err
	}

	winKeys := string(data)
	var found bool
	for _, mdi := range md.Items {
		if mdi.Key == "windows-keys" {
			val := fmt.Sprintf("%s\n%s", *mdi.Value, winKeys)
			mdi.Value = &val
			found = true
			break
		}
	}
	if !found {
		md.Items = append(md.Items, &compute.MetadataItems{Key: "windows-keys", Value: &winKeys})
	}

	fmt.Println("Setting new 'windows-keys' metadata")
	if err := client.SetInstanceMetadata(p, z, i, md); err != nil {
		return "", err
	}

	fmt.Println("Fetching encrypted password")
	var trys int
	var ep string
	for {
		time.Sleep(1 * time.Second)
		ep, err = getEncryptedPassword(client, i, z, p, winKey.Modulus)
		if err == nil {
			break
		}
		if trys > 10 {
			return "", err
		}
		trys++
	}

	fmt.Println("Decrypting password")
	return decryptPassword(key, ep)
}

func main() {
	flag.Parse()
	if *instance == "" {
		log.Fatal("-instance flag required")
	}
	if *zone == "" {
		log.Fatal("-zone flag required")
	}
	if *project == "" {
		log.Fatal("-project flag required")
	}
	if *user == "" {
		log.Fatal("-user flag required")
	}

	ctx := context.Background()
	client, err := daisyCompute.NewClient(ctx)
	if err != nil {
		log.Fatalf("Error creating compute service: %v", err)
	}

	fmt.Printf("Resetting password on instance %q for user %q\n", *instance, *user)
	pw, err := resetPassword(client, *instance, *zone, *project, *user)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("- Username: %s\n- Password: %s\n", *user, pw)
}

Python


#!/usr/bin/env python

# Copyright 2015 Google Inc. All Rights Reserved.
#
# 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.

import base64
import copy
import datetime
import json
import time

# PyCrypto library: https://pypi.python.org/pypi/pycrypto
from Crypto.Cipher import PKCS1_OAEP
from Crypto.PublicKey import RSA
from Crypto.Util.number import long_to_bytes

# Google API Client Library for Python:
# https://developers.google.com/api-client-library/python/start/get_started
from oauth2client.client import GoogleCredentials
from googleapiclient.discovery import build


def GetCompute():
    """Get a compute object for communicating with the Compute Engine API."""
    credentials = GoogleCredentials.get_application_default()
    compute = build('compute', 'v1', credentials=credentials)
    return compute


def GetInstance(compute, instance, zone, project):
    """Get the data for a Google Compute Engine instance."""
    cmd = compute.instances().get(instance=instance, project=project,
                                  zone=zone)
    return cmd.execute()


def GetKey():
    """Get an RSA key for encryption."""
    # This uses the PyCrypto library
    key = RSA.generate(2048)
    return key


def GetModulusExponentInBase64(key):
    """Return the public modulus and exponent for the key in bas64 encoding."""
    mod = long_to_bytes(key.n)
    exp = long_to_bytes(key.e)

    modulus = base64.b64encode(mod)
    exponent = base64.b64encode(exp)

    return modulus, exponent


def GetExpirationTimeString():
    """Return an RFC3339 UTC timestamp for 5 minutes from now."""
    utc_now = datetime.datetime.utcnow()
    # These metadata entries are one-time-use, so the expiration time does
    # not need to be very far in the future. In fact, one minute would
    # generally be sufficient. Five minutes allows for minor variations
    # between the time on the client and the time on the server.
    expire_time = utc_now + datetime.timedelta(minutes=5)
    return expire_time.strftime('%Y-%m-%dT%H:%M:%SZ')


def GetJsonString(user, modulus, exponent, email):
    """Return the JSON string object that represents the windows-keys entry."""
    expire = GetExpirationTimeString()
    data = {'userName': user,
            'modulus': modulus,
            'exponent': exponent,
            'email': email,
            'expireOn': expire}
    return json.dumps(data)


def UpdateWindowsKeys(old_metadata, metadata_entry):
    """Return updated metadata contents with the new windows-keys entry."""
    # Simply overwrites the "windows-keys" metadata entry. Production code may
    # want to append new lines to the metadata value and remove any expired
    # entries.
    new_metadata = copy.deepcopy(old_metadata)
    new_metadata['items'] = [{
        'key': "windows-keys",
        'value': metadata_entry
    }]
    return new_metadata


def UpdateInstanceMetadata(compute, instance, zone, project, new_metadata):
    """Update the instance metadata."""
    cmd = compute.instances().setMetadata(instance=instance, project=project,
                                          zone=zone, body=new_metadata)
    return cmd.execute()


def GetSerialPortFourOutput(compute, instance, zone, project):
    """Get the output from serial port 4 from the instance."""
    # Encrypted passwords are printed to COM4 on the windows server:
    port = 4
    cmd = compute.instances().getSerialPortOutput(instance=instance,
                                                  project=project,
                                                  zone=zone, port=port)
    output = cmd.execute()
    return output['contents']


def GetEncryptedPasswordFromSerialPort(serial_port_output, modulus):
    """Find and return the correct encrypted password, based on the modulus."""
    # In production code, this may need to be run multiple times if the output
    # does not yet contain the correct entry.
    output = serial_port_output.split('\n')
    for line in reversed(output):
        try:
            entry = json.loads(line)
            if modulus == entry['modulus']:
                return entry['encryptedPassword']
        except ValueError:
            pass


def DecryptPassword(encrypted_password, key):
    """Decrypt a base64 encoded encrypted password using the provided key."""
    decoded_password = base64.b64decode(encrypted_password)
    cipher = PKCS1_OAEP.new(key)
    password = cipher.decrypt(decoded_password)
    return password


def main(instance, zone, project, user, email):
    # Setup
    compute = GetCompute()
    key = GetKey()
    modulus, exponent = GetModulusExponentInBase64(key)

    # Get existing metadata
    instance_ref = GetInstance(compute, instance, zone, project)
    old_metadata = instance_ref['metadata']

    # Create and set new metadata
    metadata_entry = GetJsonString(user, modulus,
                                   exponent, email)
    new_metadata = UpdateWindowsKeys(old_metadata, metadata_entry)
    result = UpdateInstanceMetadata(compute, instance, zone, project,
                                    new_metadata)

    # For this sample code, just sleep for 30 seconds instead of checking for
    # responses. In production code, this should monitor the status of the
    # metadata update operation.
    time.sleep(30)

    # Get and decrypt password from serial port output
    serial_port_output = GetSerialPortFourOutput(compute, instance,
                                                 zone, project)
    enc_password = GetEncryptedPasswordFromSerialPort(serial_port_output,
                                                      modulus)
    password = DecryptPassword(enc_password, key)

    # Display the username, password and IP address for the instance
    print 'Username:   {0}'.format(user)
    print 'Password:   {0}'.format(password)
    ip = instance_ref['networkInterfaces'][0]['accessConfigs'][0]['natIP']
    print 'IP Address: {0}'.format(ip)


if __name__ == '__main__':
    instance = 'my-instance'
    zone = 'us-central1-a'
    project = 'my-project'
    user = 'example-user'
    email = 'user@example.com'
    main(instance, zone, project, user, email)

Java


/**
 * Copyright 2015 Google Inc. All Rights Reserved.
 *
 * 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.
 */

/**
 * This package demonstrates how to reset Windows passwords in Java.
 */

package cloud.google.com.windows.example;


import com.google.api.client.auth.oauth2.Credential;
import com.google.api.client.googleapis.auth.oauth2.GoogleCredential;
import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.json.JsonFactory;
import com.google.api.client.json.jackson2.JacksonFactory;
import com.google.api.client.repackaged.org.apache.commons.codec.binary.Base64;
import com.google.api.services.compute.Compute;
import com.google.api.services.compute.model.Instance;
import com.google.api.services.compute.model.Metadata;
import com.google.api.services.compute.model.Metadata.Items;
import com.google.api.services.compute.model.SerialPortOutput;
import com.google.common.io.BaseEncoding;

import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;

import java.math.BigInteger;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.Security;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.RSAPublicKeySpec;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import java.util.TimeZone;

import javax.crypto.Cipher;

public class ExampleCode {

  public ExampleCode() {}

  // Constants used to configure behavior.
  private static final String ZONE_NAME = "us-central1-a";
  private static final String PROJECT_NAME = "example-project-1234";
  private static final String INSTANCE_NAME = "test-instance";
  private static final String APPLICATION_NAME = "windows-pw-reset";

  // Constants for configuring user name, email, and SSH key expiration.
  private static final String USER_NAME = "example_user";
  private static final String EMAIL = "example_user@test.com";

  // Keys are one-time use, so the metadata doesn't need to stay around for long.
  // 5 minutes chosen to allow for differences between time on the client
  // and time on the server.
  private static final long EXPIRE_TIME = 300000;

  // HttpTransport and JsonFactory used to create the Compute object.
  private static HttpTransport httpTransport;
  private static final JsonFactory JSON_FACTORY = JacksonFactory.getDefaultInstance();



  public static void main(String[] args) {
    ExampleCode ec = new ExampleCode();
    try {
      // Initialize Transport object.
      httpTransport = GoogleNetHttpTransport.newTrustedTransport();

      // Reset the password.
      ec.resetPassword();
    } catch (Exception e) {
      e.printStackTrace();
      System.exit(1);
    }
  }

  public void resetPassword() throws Exception {
    // Get credentials to setup a connection with the Compute API.
    Credential cred = GoogleCredential.getApplicationDefault();

    // Create an instance of the Compute API.
    Compute compute = new Compute.Builder(httpTransport, JSON_FACTORY, null)
        .setApplicationName(APPLICATION_NAME).setHttpRequestInitializer(cred).build();

    // Get the instance object to gain access to the instance's metadata.
    Instance inst = compute.instances().get(PROJECT_NAME, ZONE_NAME, INSTANCE_NAME).execute();
    Metadata metadata = inst.getMetadata();

    // Generate the public/private key pair for encryption and decryption.
    KeyPair keys = generateKeys();

    // Update metadata from instance with new windows-keys entry.
    replaceMetadata(metadata, buildKeyMetadata(keys));

    // Tell Compute Engine to update the instance metadata with our changes.
    compute.instances().setMetadata(PROJECT_NAME, ZONE_NAME, INSTANCE_NAME, metadata).execute();

    System.out.println("Updating metadata...");

    // Sleep while waiting for metadata to propagate - production code may
    // want to monitor the status of the metadata update operation.
    Thread.sleep(30000);

    System.out.println("Getting serial output...");

    // Request the output from serial port 4.
    // In production code, this operation should be polled.
    SerialPortOutput output = compute.instances()
        .getSerialPortOutput(PROJECT_NAME, ZONE_NAME, INSTANCE_NAME).setPort(4).execute();

    // Get the last line - this will be a JSON string corresponding to the
    // most recent password reset attempt.
    String[] entries = output.getContents().split("\n");
    String outputEntry = entries[entries.length - 1];

    // Parse output using the json-simple library.
    JSONParser parser = new JSONParser();
    JSONObject passwordDict = (JSONObject) parser.parse(outputEntry);

    String encryptedPassword = passwordDict.get("encryptedPassword").toString();

    // Output user name and decrypted password.
    System.out.println("\nUser name: " + passwordDict.get("userName").toString());
    System.out.println("Password: " + decryptPassword(encryptedPassword, keys));
  }

  private String decryptPassword(String message, KeyPair keys) {
    try {
      // Add the bouncycastle provider - the built-in providers don't support RSA
      // with OAEPPadding.
      Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());

      // Get the appropriate cipher instance.
      Cipher rsa = Cipher.getInstance("RSA/NONE/OAEPPadding", "BC");

      // Add the private key for decryption.
      rsa.init(Cipher.DECRYPT_MODE, keys.getPrivate());

      // Decrypt the text.
      byte[] rawMessage = Base64.decodeBase64(message);
      byte[] decryptedText = rsa.doFinal(rawMessage);

      // The password was encoded using UTF8. Transform into string.
      return new String(decryptedText, "UTF8");
    } catch (Exception e) {
      e.printStackTrace();
      System.exit(1);
    }
    return "";
  }

  private void replaceMetadata(Metadata input, JSONObject newMetadataItem) {
    // Transform the JSON object into a string that the API can use.
    String newItemString = newMetadataItem.toJSONString();

    // Get the list containing all of the Metadata entries for this instance.
    List<Items> items = input.getItems();

    // If the instance has no metadata, items can be returned as null.
    if (items == null)
    {
      items = new LinkedList<Items>();
      input.setItems(items);
    }

    // Find the "windows-keys" entry and update it.
    for (Items item : items) {
      if (item.getKey().compareTo("windows-keys") == 0) {
        // Replace item's value with the new entry.
        // To prevent race conditions, production code may want to maintain a
        // list where the oldest entries are removed once the 32KB limit is
        // reached for the metadata entry.
        item.setValue(newItemString);
        return;
      }
    }

    // "windows.keys" entry doesn't exist in the metadata - append it.
    // This occurs when running password-reset for the first time on an instance.
    items.add(new Items().setKey("windows-keys").setValue(newItemString));
  }

  private KeyPair generateKeys() throws NoSuchAlgorithmException {
    KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");

    // Key moduli for encryption/decryption are 2048 bits long.
    keyGen.initialize(2048);

    return keyGen.genKeyPair();
  }


  @SuppressWarnings("unchecked")
  private JSONObject buildKeyMetadata(KeyPair pair) throws NoSuchAlgorithmException,
      InvalidKeySpecException {
    // Object used for storing the metadata values.
    JSONObject metadataValues = new JSONObject();

    // Encode the public key into the required JSON format.
    metadataValues.putAll(jsonEncode(pair));

    // Add username and email.
    metadataValues.put("userName", USER_NAME);
    metadataValues.put("email", EMAIL);

    // Create the date on which the new keys expire.
    Date now = new Date();
    Date expireDate = new Date(now.getTime() + EXPIRE_TIME);

    // Format the date to match rfc3339.
    SimpleDateFormat rfc3339Format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
    rfc3339Format.setTimeZone(TimeZone.getTimeZone("UTC"));
    String dateString = rfc3339Format.format(expireDate);

    // Encode the expiration date for the returned JSON dictionary.
    metadataValues.put("expireOn", dateString);

    return metadataValues;
  }

  @SuppressWarnings("unchecked")
  private JSONObject jsonEncode(KeyPair keys) throws NoSuchAlgorithmException,
      InvalidKeySpecException {
    KeyFactory factory = KeyFactory.getInstance("RSA");

    // Get the RSA spec for key manipulation.
    RSAPublicKeySpec pubSpec = factory.getKeySpec(keys.getPublic(), RSAPublicKeySpec.class);

    // Extract required parts of the key.
    BigInteger modulus = pubSpec.getModulus();
    BigInteger exponent = pubSpec.getPublicExponent();

    // Grab an encoder for the modulus and exponent to encode using RFC 3548;
    // Java SE 7 requires an external library (Google's Guava used here)
    // Java SE 8 has a built-in Base64 class that can be used instead. Apache also has an RFC 3548
    // encoder.
    BaseEncoding stringEncoder = BaseEncoding.base64();

    // Strip out the leading 0 byte in the modulus.
    byte[] arr = Arrays.copyOfRange(modulus.toByteArray(), 1, modulus.toByteArray().length);

    JSONObject returnJson = new JSONObject();

    // Encode the modulus, add to returned JSON object.
    String modulusString = stringEncoder.encode(arr).replaceAll("\n", "");
    returnJson.put("modulus", modulusString);

    // Encode exponent, add to returned JSON object.
    String exponentString = stringEncoder.encode(exponent.toByteArray()).replaceAll("\n", "");
    returnJson.put("exponent", exponentString);

    return returnJson;
  }
}

手動での手順

この手動設定ガイドの手順では、暗号機能に OpenSSL を、その他の機能に Bash shell ツールと Linux ツールを使用しますが、他の多くのツールを実装することも可能です。

  1. 2,048 ビットの RSA 鍵ペアを生成します。OpenSSL で、次を実行してこの鍵ペアを生成します。

    $ openssl genrsa -out private_key 2048
    

    これで、以下のような内容の private_key という名前の秘密鍵ファイルが作成されます。

    $ cat private_key
    -----BEGIN RSA PRIVATE KEY-----
    MIIEpAIBAAKCAQEAwgsquN4IBNPqIUnu+h/5Za1kujb2YRhX1vCQVQAkBwnWigcC
    qOBVfRa5JoZfx6KIvEXjWqa77jPvlsxM4WPqnDIM2qiK36up3SKkYwFjff6F2ni/
    ry8vrwXCX3sGZ1hbIHlK0O012HpA3ISeEswVZmX2X67naOvJXfY5v0hGPWqCADao
    +xVxrmxsZD4IWnKl1UaZzI5lhAzr8fw6utHwx1EZ/MSgsEki6tujcZfN+GUDRnmJ
    GQSnPTXmsf7Q4DKreTZk49cuyB3prV91S0x3DYjCUpSXrkVy1Ha5XicGD/q+ystu
    FsJnrrhbNXJbpSjM6sjo/aduAkZJl4FmOt0R7QIDAQABAoIBAQCsT6hHc/tg9iIC
    H5pUiRI55Uj+R5JwVGKkXwl8Qdy8V1MpTOJivpuLsiMGf+sL51xO/CzRsiBOfdYz
    bgaTW9vZimR5w5NW3iTAV2Ps+y2zk9KfV/y3/0nzvUSG70OXgBGj+7GhaBQZwS5Z
    5HZOsOYMAV1QSIv8Uu2FQAK1xuOA4seJ/NK42iXgVB1XvYe2AxCWNqCBJylk9F5N
    8a213oJWw2mwQWCSfZhuvwYRO7w/V+mInKPkKlWvf3SLuMCWeDI8s0jLsJMQ0rbp
    jYXRzc2G+LF1aLxjatiGeLsqfVYerNohufGAajpNkSvcMciDXvD9aJhZqior+x2Q
    rCnMuNRNAoGBAPI6r32wIf8H9GmcvGrXk9OYLq0uJGqAtJDgGmJM5BSX4mlSz+Ni
    SYlQOfi24ykQDo3XbA59Lb6H0L64czi2a3OmpG8s6h4ymp+3cSd1k1AER1oZudwH
    9UScGfSgT/nMgufBwEGlQkCMp5x4Sl20clCHZ49p9eNiXML3wxpCZPIjAoGBAM0T
    NKt/rjqMs0qOWAJKemSPk0zV+1RSjCoOdKC6jmHRGr/MIoKiJLIkywV2m53yv8Wu
    BF3gVUDlwojoOKcVR8588tek5L0j9RshGovKj4Uxz9uPPhzeNnlSA+5PS284VtKz
    LX8xZ/b+MNCyor9jT0qoWylqym0w+M4aFL2tUQSvAoGABJvnQO38B51AIk5QK3xE
    nM8VfEgXe0tNpEAPYHV0FYw6S6S+veXd3lX/dGMOeXaLwFkr/i6Vkz2EVEywLJEU
    BFRUZqUlI0P1OzrDVWvgTLJ4JRe+OJiSKycJO2VdgDRK/Vvra5RYaWADxG9pgtTv
    I+cfqlPq0NPLTg5m0PYYc58CgYBpGt/SygTNA1Hc82mN+wgRxDhVmBJRHGG0KGaD
    /jl9TsOr638AfwPZvdvD+A83+7NoKJEaYCCxu1BiBMsMb263GPkJpvyJKAW2mtfV
    L8MxG9+Rgy/tccJvmaZkHIXoAfMV2DmISBUl1Q/F1thsyQRZmkHmz1Hidsf+MgXR
    VSQCBwKBgQCxwJtGZGPdQbDXcZZtL0yJJIbdt5Q/TrW0es17IPAoze+E6zFg9mo7
    ea9AuGxOGDQwO9n5DBn/3XcSjRnhvXaW60Taz6ZC60Zh/s6IilCmav+n9ewFHJ3o
    AglSJZRJ1Eer0m5m6s2FW5U0Yjthxwkm3WCWS61cOOTvb6xhQ5+WSw==
    -----END RSA PRIVATE KEY-----
    
  2. 公開鍵を生成します。公開鍵を作成するには、以下を実行します。

    $ openssl rsa -pubout -in private_key -out public_key
    

    次のような public_key ファイルが作成されます。

    $ cat public_key
    -----BEGIN PUBLIC KEY-----
    MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwgsquN4IBNPqIUnu+h/5
    Za1kujb2YRhX1vCQVQAkBwnWigcCqOBVfRa5JoZfx6KIvEXjWqa77jPvlsxM4WPq
    nDIM2qiK36up3SKkYwFjff6F2ni/ry8vrwXCX3sGZ1hbIHlK0O012HpA3ISeEswV
    ZmX2X67naOvJXfY5v0hGPWqCADao+xVxrmxsZD4IWnKl1UaZzI5lhAzr8fw6utHw
    x1EZ/MSgsEki6tujcZfN+GUDRnmJGQSnPTXmsf7Q4DKreTZk49cuyB3prV91S0x3
    DYjCUpSXrkVy1Ha5XicGD/q+ystuFsJnrrhbNXJbpSjM6sjo/aduAkZJl4FmOt0R
    7QIDAQAB
    -----END PUBLIC KEY-----
    
  3. 係数と指数を抽出します。係数と指数が、公開鍵と秘密鍵を構成します。公開鍵から、係数と指数を抽出します。

    $ openssl rsa -in public_key -pubin -text -noout
    Public-Key: (2048 bit)
    Modulus:
        00:c2:0b:2a:b8:de:08:04:d3:ea:21:49:ee:fa:1f:
        f9:65:ad:64:ba:36:f6:61:18:57:d6:f0:90:55:00:
        24:07:09:d6:8a:07:02:a8:e0:55:7d:16:b9:26:86:
        5f:c7:a2:88:bc:45:e3:5a:a6:bb:ee:33:ef:96:cc:
        4c:e1:63:ea:9c:32:0c:da:a8:8a:df:ab:a9:dd:22:
        a4:63:01:63:7d:fe:85:da:78:bf:af:2f:2f:af:05:
        c2:5f:7b:06:67:58:5b:20:79:4a:d0:ed:35:d8:7a:
        40:dc:84:9e:12:cc:15:66:65:f6:5f:ae:e7:68:eb:
        c9:5d:f6:39:bf:48:46:3d:6a:82:00:36:a8:fb:15:
        71:ae:6c:6c:64:3e:08:5a:72:a5:d5:46:99:cc:8e:
        65:84:0c:eb:f1:fc:3a:ba:d1:f0:c7:51:19:fc:c4:
        a0:b0:49:22:ea:db:a3:71:97:cd:f8:65:03:46:79:
        89:19:04:a7:3d:35:e6:b1:fe:d0:e0:32:ab:79:36:
        64:e3:d7:2e:c8:1d:e9:ad:5f:75:4b:4c:77:0d:88:
        c2:52:94:97:ae:45:72:d4:76:b9:5e:27:06:0f:fa:
        be:ca:cb:6e:16:c2:67:ae:b8:5b:35:72:5b:a5:28:
        cc:ea:c8:e8:fd:a7:6e:02:46:49:97:81:66:3a:dd:
        11:ed
    Exponent: 65537 (0x10001)
    
  4. 係数と指数をエンコードします。係数と指数を抽出し、Base64 エンコードする必要があります。係数をエンコードする前に、係数からリーディング ゼロバイトを削除します。デフォルトでは、public_key ファイルは以下の情報を含むバイトの Base64 エンコード文字列です。

    • 32 バイトのヘッダー情報
    • リーディング ゼロを含む 1 バイトの係数
    • 256 バイトの係数
    • 2 バイトの指数ヘッダー
    • 3 バイトの指数

    係数と指数は、他のファイル コンテンツとは別に抽出し、エンコードする必要があります。次のコマンドを使用して、係数と指数を抽出し、エンコードします。

    $ cat public_key | grep -v -- ----- | base64 -d | dd bs=1 skip=33 count=256 2>/dev/null | base64 -w 0; echo
            wgsquN4IBNPqIUnu+h/5Za1kujb2YRhX1vCQVQAkBwnWigcCqOBVfRa5JoZfx6KIvEXjWqa77jPvlsx
    M4WPqnDIM2qiK36up3SKkYwFjff6F2ni/ry8vrwXCX3sGZ1hbIHlK0O012HpA3ISeEswVZmX2X67naO
    vJXfY5v0hGPWqCADao+xVxrmxsZD4IWnKl1UaZzI5lhAzr8fw6utHwx1EZ/MSgsEki6tujcZfN+GUDR
    nmJGQSnPTXmsf7Q4DKreTZk49cuyB3prV91S0x3DYjCUpSXrkVy1Ha5XicGD/q+ystuFsJnrrhbNXJb
    pSjM6sjo/aduAkZJl4FmOt0R7Q==
    
    $ cat public_key | grep -v -- ----- | base64 -d | dd bs=1 skip=291 count=3 2>/dev/null | base64
    AQAB
    

    係数のエンコード中に問題が起きた場合、エンコード前に係数からリーディング ゼロバイトが削除されているか確認してください。

  5. ユーザー名と公開鍵の情報を使用して、JSON オブジェクトを作成します。次のデータを使用して、JSON オブジェクトを作成してください。

    • userName: インスタンスにログインする際のユーザー名。
    • modulus: Base64 エンコードされた、公開鍵の係数。
    • exponent: Base64 エンコードされた、公開鍵の指数。
    • email: パスワードをリクエストしているユーザーのメールアドレス。このメールアドレスは、API 認証済み Google アカウントのメールアドレスにする必要があります。
    • expireOn: 鍵の有効期限を示す RFC 3399 エンコードのタイムスタンプ。これは UTC 時刻に基づいて、約 5 分後に設定します。これらの鍵は、ユーザー名とパスワードの生成にのみ使用するため、パスワード作成後には必要なくなります。エージェントは、有効期限の切れた鍵は使用しません。

    例:

    {\"userName\": \"example-user\",  \"modulus\": \"wgsquN4IBNPqIUnu+h/5Za1kujb2YRhX1
    vCQVQAkBwnWigcCqOBVfRa5JoZfx6KIvEXjWqa77jPvlsxM4WPqnDIM2qiK36up3SKkYwFjff6F
    2ni/ry8vrwXCX3sGZ1hbIHlK0O012HpA3ISeEswVZmX2X67naOvJXfY5v0hGPWqCADao+xVxrmx
    sZD4IWnKl1UaZzI5lhAzr8fw6utHwx1EZ/MSgsEki6tujcZfN+GUDRnmJGQSnPTXmsf7Q4DKreT
    Zk49cuyB3prV91S0x3DYjCUpSXrkVy1Ha5XicGD/q+ystuFsJnrrhbNXJbpSjM6sjo/aduAkZJl
    4FmOt0R7Q==\", \"exponent\": \"AQAB\", \"email\": \"example.user@example.com\",
    \"expireOn\": \"2015-04-14T01:37:19Z\"}
    

    JSON 文字列には改行がないよう注意してください。

  6. JSON オブジェクトをインスタンス メタデータに追加します。メタデータの鍵 windows-keys と JSON オブジェクトを鍵の値として使用し、インスタンス メタデータを設定します。

    API でインスタンス メタデータを更新するには、リクエスト時にフィンガープリントを入力する必要があります。インスタンスに GET リクエストを送り、最新のフィンガープリントを取得してください。

    GET  https://compute.googleapis.com/compute/v1/projects/myproject/zones/us-central1-f/instances/test-windows-auth
    [..snip..]
    "metadata": {
    "kind": "compute#metadata",
    "fingerprint": "5sFotm8Ee0I=",
    "items": [
     {
     …
     }
    [..snip]..
    

    次に、setMetadata メソッドに対して POST リクエストを実行し、フィンガープリントと作成した JSON オブジェクトを渡します。

    POST https://compute.googleapis.com/compute/v1/projects/myproject/zones/us-central1-f/instances/test-windows-auth/setMetadata
    
    {
     "fingerprint": "5sFotm8Ee0I=",
     "items": [
      {
       "value": "{\"userName\": \"example-user\",  \"modulus\": \"wgsquN4IBNPqIUnu+h/5Za1kujb2YRhX1vCQVQAkBwnWigcCqOBVfRa5JoZfx6KIvEXjWqa77jPvlsxM4WPqnDIM2qiK36up3SKkYwFjff6F2ni/ry8vrwXCX3sGZ1hbIHlK0O012HpA3ISeEswVZmX2X67naOvJXfY5v0hGPWqCADao+xVxrmxsZD4IWnKl1UaZzI5lhAzr8fw6utHwx1EZ/MSgsEki6tujcZfN+GUDRnmJGQSnPTXmsf7Q4DKreTZk49cuyB3prV91S0x3DYjCUpSXrkVy1Ha5XicGD/q+ystuFsJnrrhbNXJbpSjM6sjo/aduAkZJl4FmOt0R7Q==\", \"exponent\": \"AQAB\", \"email\": \"user@example.com\", \"expireOn': '2015\"04-14T01:37:19Z\"}\n",
       "key": "windows-keys"
      } ]
    }
    

    鍵の名前が windows-keys になっていて、値は上記のように 1 つまたは複数の JSON 文字列に設定されている必要があります。複数の文字列は改行で区切る必要があります。複数のエントリを追加する場合は、メタデータ値が 32 KB を超えないようにしてください。

  7. シリアルポート 4 番から、出力を読み取ります。インスタンスのエージェントは自動的に windows-keys の値を取得し、暗号化されたパスワードを作成します。シリアルポート 4 番にクエリを行い、暗号化されたパスワードを読み取ります。API で、getSerialPortOutput メソッドに対して GET リクエストを実行し、クエリ パラメータとして port=4 を渡します。

    GET https://compute.googleapis.com/compute/v1/projects/myproject/zones/us-central1-f/instances/test-windows-auth/serialPort?port=4
    
    {
     "kind": "compute#serialPortOutput",
     "selfLink": "https://www.googleapis.com/compute/v1/projects/myproject/zones/_/instances/test-api-auth/serialPort",
     "contents": "{\"ready\":true,\"version\":\"Microsoft Windows NT 6.1.7601 Service Pack 1\"}\n{\"encryptedPassword\":\"uiHDEhxyvj6lF5GalH
     h9TsMZb4bG6Y9qGmFb9S3XI29yvVsDCLdp4IbUg21MncHcaxP0rFu0kyjxlEXDs8y4L1KOhy6iyB42Lh+vZ4XIMjmvU4rZrjsBZ5TxQo9hL0lBW7o3FRM\\/UIXCeRk39ObUl2A
     jDmQ0mcw1byJI5v9KVJnNMaHdRCy\\/kvN6bx3qqjIhIMu0JExp4UVkAX2Mxb9b+c4o2DiZF5pY6ZfbuEmjSbvGRJXyswkOJ4jTZl+7e6+SZfEal8HJyRfZKiqTjrz+DLjYSlXr
     fIRqlvKeAFGOJq6IRojNWiTOOh8Zorc0iHDTIkf+MY0scfbBUo5m30Bf4w==\",\"exponent\":\"AQAB\",\"modulus\":\"0tiKdO2JmBHss26jnrSAwb583KG\\/ZIw5Jw
     wMPXrCVsFAPwY1OV3RlT1Hp4Xvpibr7rvJbOC+f\\/Gd0cBrK5pccQfccB+OHKpbBof473zEfRbdtFwPn10RfAFj\\/xikW0r\\/XxgG\\/c8tz9bmALBStGqmwOVOLRHxjwgtG
     u4poeuwmFfG6TuwgCadxpllW74mviFd4LZVSuCSni5YJnBM2HSJ8NP6g1fqI17KDXt2XO\\/7kSItubmMk+HGEXdH4qiugHYewaIf1o4XSQROC8xlRl7t\\/RaD4U58hKYkVwg0
     Ir7WzYzAVpG2UR4Co\\/GDG9Hct7HOYekDqVQ+sSZbwzajnVunkw==\",\"passwordFound\":true,\"userName\":\"example-user\"}\n"
    }
    

    シリアルポートの出力には、改行で区切られた複数のレスポンスがある場合があります。正しいレスポンスを見つけるには、指定した係数とシリアルポートの出力を一致させます。各レスポンスは、JSON エンコードされた文字列で、次のフィールドがあります。

    • userName: インスタンスに送られたユーザー名。
    • passwordFound: パスワードの生成が成功したかを示すブール値。
    • encryptedPassword: Base64 でエンコードされ、暗号化されたパスワード。
    • modulus: 以前に送られた係数。
    • exponent: 以前に送られた指数。

    シリアルポート出力の保存については、シリアルポート出力の表示をご覧ください。

  8. パスワードを復号します。パスワードを取得するには、前の手順で作成した秘密鍵を使用し、暗号化されたパスワードを復号します。パスワードは、Optimal Asymmetric Encryption Padding(OAEP)を使用して復号する必要があります。OpenSSL では、次のコマンドで入力データを復号します。

    $ openssl rsautl -decrypt -inkey private_key -oaep
    

    上記のパスワードを復号するには、encryptedPassword 値を入力します。必ず文字列からエスケープ文字 \\ を前もって削除しておかないと、復号に失敗します。

    $ echo 'uiHDEhxyvj6lF5GalHh9TsMZb4bG6Y9qGmFb9S3XI291MncHcaxP0rFu0kyjxlEXDs8y4L1KOhy6iyB42Lh+vZ4XIMjmvU4rZrjsBZ5Tx
    Qo9hL0lBW7o3FRM/UIXCeRk39ObUl2AjDmQ0mcw1byJI5v9KVJnNMaHdRCy/kvN6bx3qqjIhIMu0JExp4UVkAX2Mxb9b+c4o2DiZF5pY6ZfbuEmjS
    bvGRJXyswkOJ4jTZl+7e6+SZfEal8HJyRfZKiqTjrz+DLjYSlXrfIRqlvKeAFGOJq6IRojNWiTOOh8Zorc0iHDTIkf+MY0scfbBUo5m30Bf4w==' |
    base64 -d | openssl rsautl -decrypt -inkey private_key -oaep
    

    コマンドにより、復号されたパスワードが出力されます。

    dDkJ_3]*QYS-#>X
    

    このアカウントのユーザー名とパスワードは、次のようになります。

    username: example-user
    password: dDkJ_3]*QYS-#>X
    
  9. 鍵を削除します。SSH 認証鍵とは異なり、Windows パスワードの取得、リセットに使用する鍵は一時的なものです。公開鍵 / 秘密鍵の再利用は、想定どおりに機能しない可能性があるため、おすすめしません。鍵がディスクに保存されている場合は、プロセスの最後にファイルを消去する必要があります。可能ならば、鍵はメモリに保存し、プロセスが完了したら破棄してください。

次のステップ