| // Copyright 2021 Google LLC | |
| // | |
| // 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 devicepolicy | |
| // Implementation of the Manager interface for managing Chrome OS device policy. | |
| // For more detail on Chrome OS device policy, see | |
| // http://www.chromium.org/developers/how-tos/enterprise/protobuf-encoded-policy-blobs | |
| import ( | |
| "crypto/rand" | |
| "crypto/rsa" | |
| "crypto/x509" | |
| "errors" | |
| "io/ioutil" | |
| "os" | |
| "path/filepath" | |
| pmpb "policy-manager/protos" | |
| "reflect" | |
| "github.com/golang/glog" | |
| "github.com/golang/protobuf/proto" | |
| ) | |
| const ( | |
| // policyType is a Chrome OS constant to specify that the policy data | |
| // refers to a device policy. | |
| // See line 2 in third_party/chromium/devicepolicyproto/device_management_backend.proto. | |
| policyType = "google/chromeos/device" | |
| // configFilePerm is the permission for the COS policy file. | |
| configFilePerm = os.FileMode(0644) | |
| // keyFilePerm is the permission for public/private key files. | |
| keyFilePerm = os.FileMode(0600) | |
| // rsaKeyBits is the number of bits to use to generate the private key. | |
| rsaKeyBits = 1024 | |
| ) | |
| // createKeyFiles creates a new public and private key pair and writes them to | |
| // disk. | |
| // Note that when the key files are being created, any existing policy cannot be | |
| // read and verified by Chrome OS components such as update_engine. | |
| func createKeyFiles(publicKeyFile string, privateKeyFile string) (*rsa.PrivateKey, error) { | |
| // Generate RSA key pair. | |
| key, err := rsa.GenerateKey(rand.Reader, rsaKeyBits) | |
| if err != nil { | |
| return nil, err | |
| } | |
| // Serialize the public key to DER-encoded PKIX format and output to | |
| // file. | |
| publicKeyBytes, err := x509.MarshalPKIXPublicKey(key.Public()) | |
| if err != nil { | |
| return nil, err | |
| } | |
| err = ioutil.WriteFile(publicKeyFile, publicKeyBytes, keyFilePerm) | |
| if err != nil { | |
| return nil, err | |
| } | |
| // Serialize the private key to ASN.1 DER encoded form and output to | |
| // file. | |
| privateKeyBytes := x509.MarshalPKCS1PrivateKey(key) | |
| if err != nil { | |
| return nil, err | |
| } | |
| err = ioutil.WriteFile(privateKeyFile, privateKeyBytes, keyFilePerm) | |
| if err != nil { | |
| return nil, err | |
| } | |
| glog.V(1).Info("new keys created") | |
| return key, nil | |
| } | |
| // checkKeyFiles checks the private/public key files are present on disk and | |
| // form a valid key pair. | |
| // An error is returned if the private or public key file is not present or if | |
| // the keys don't match. | |
| // If no error, then the private key is returned. | |
| func checkKeyFiles(publicKeyFile string, privateKeyFile string) (*rsa.PrivateKey, error) { | |
| // Get private key. | |
| privateKeyBytes, err := ioutil.ReadFile(privateKeyFile) | |
| if err != nil { | |
| return nil, err | |
| } | |
| privateKey, err := x509.ParsePKCS1PrivateKey(privateKeyBytes) | |
| if err != nil { | |
| return nil, err | |
| } | |
| // Get public key. | |
| publicKeyBytes, err := ioutil.ReadFile(publicKeyFile) | |
| if err != nil { | |
| return nil, err | |
| } | |
| publicKey, err := x509.ParsePKIXPublicKey(publicKeyBytes) | |
| if err != nil { | |
| return nil, err | |
| } | |
| // Verify that the public and private keys match. | |
| if !reflect.DeepEqual(publicKey, privateKey.Public()) { | |
| return nil, errors.New("public and private keys don't match") | |
| } | |
| return privateKey, nil | |
| } | |
| // getKeys returns the private key of the public/private key pair present on | |
| // disk. If no valid key pair exsits, an error will be returned unless the | |
| // caller has set generateIfMissing to true. | |
| func getKeys(publicKeyFile string, privateKeyFile string, generateIfMissing bool) (*rsa.PrivateKey, error) { | |
| // generateKeysIfNeeded is called when there is no valid key pair on | |
| // disk and should consult the generateIfMissing flag to decide wheher | |
| // new keys should be generated. | |
| generateKeysIfNeeded := func() (*rsa.PrivateKey, error) { | |
| if generateIfMissing { | |
| return createKeyFiles(publicKeyFile, privateKeyFile) | |
| } | |
| return nil, errors.New("missing key files") | |
| } | |
| // Check if valid public/private key files are present. | |
| _, errPrivateMissing := os.Stat(privateKeyFile) | |
| _, errPublicMissing := os.Stat(publicKeyFile) | |
| if os.IsNotExist(errPrivateMissing) || os.IsNotExist(errPublicMissing) { | |
| glog.V(1).Info("keys missing") | |
| return generateKeysIfNeeded() | |
| } else if privateKey, err := checkKeyFiles(publicKeyFile, privateKeyFile); err != nil { | |
| // The keys on disk do not form a valid private/public key pair. | |
| glog.V(1).Infof("invalid key pair: %v", err) | |
| return generateKeysIfNeeded() | |
| } else { | |
| return privateKey, nil | |
| } | |
| } | |
| // GetDefaultInstanceConfigFromBase returns the instance config derived from given | |
| // baseConfig. All missing fields in baseConfig are populated with default values. | |
| func GetDefaultInstanceConfigFromBase(baseConfig *pmpb.InstanceConfig) *pmpb.InstanceConfig { | |
| // default policy disables updates, disables cos monitoring logging and monitoring. | |
| result := &pmpb.InstanceConfig{ | |
| MetricsEnabled: proto.Bool(false), | |
| UpdateStrategy: proto.String(""), | |
| HealthMonitorConfig: &pmpb.HealthMonitorConfig{ | |
| Enforced: proto.Bool(false), | |
| LoggingEnabled: proto.Bool(false), | |
| MonitoringEnabled: proto.Bool(false), | |
| }, | |
| } | |
| // Override fields from baseConfig. | |
| proto.Merge(result, baseConfig) | |
| return result | |
| } | |
| // InitDevicePolicy initializes instance's environment to support device policy. | |
| // The initialization process includes (in order): | |
| // 1. Create the directory to store policy file if it doesn't exist. | |
| // 2. New devicepolicy will be created and written to disk (overwriting any previous | |
| // settings), All missing fields will have default values. If the given InstanceConfig | |
| // is valid (not nil), new devicepolicy will be generated from it; if the given | |
| // InstanceConfig is nil, which indicates metadata server fetching error, new devicepolicy | |
| // will be generated from current devicepolicy, which is stored on disk. | |
| func InitDevicePolicy(baseConfig *pmpb.InstanceConfig, devicePolicyFile string, instanceConfigFile string, publicKeyFile string, privateKeyFile string) error { | |
| // Make sure the policy directory exists. | |
| dir := filepath.Dir(instanceConfigFile) | |
| if err := os.MkdirAll(dir, configFilePerm); err != nil { | |
| return err | |
| } | |
| // Create new keys if no valid keys are present. | |
| _, err := getKeys(publicKeyFile, privateKeyFile, true) | |
| if err != nil { | |
| return err | |
| } | |
| onDiskConfig, err := GetInstanceConfig(instanceConfigFile) | |
| // If failed to fetch config from metadata server, use the copy on disk. | |
| if baseConfig == nil && err == nil { | |
| glog.V(1).Infof("Reusing existing device policy for initialization: %v", onDiskConfig) | |
| return SetInstanceConfig(onDiskConfig, devicePolicyFile, instanceConfigFile, publicKeyFile, privateKeyFile) | |
| } | |
| // Overwrite the copy of device policy on disk using baseConfig. | |
| return SetInstanceConfig(baseConfig, devicePolicyFile, instanceConfigFile, publicKeyFile, privateKeyFile) | |
| } | |
| // SetInstanceConfig sets a new device policy from an InstanceConfig. The | |
| // new policy will atomically replace the existing policy. | |
| func SetInstanceConfig(baseConfig *pmpb.InstanceConfig, devicePolicyFile string, instanceConfigFile string, publicKeyFile string, privateKeyFile string) error { | |
| // We need to populate unspecified fields with default values. | |
| config := GetDefaultInstanceConfigFromBase(baseConfig) | |
| // Get the private key from disk. Don't generate new keys if valid | |
| // public/private key pair is missing. | |
| privateKey, err := getKeys(publicKeyFile, privateKeyFile, false) | |
| if err != nil { | |
| return err | |
| } | |
| // Read the current device policy. Ignore error if policy file is missing. | |
| policy := new(DevicePolicy) | |
| if err := policy.ReadPolicyFromDisk(instanceConfigFile); err != nil { | |
| return err | |
| } | |
| currentConfig := new(pmpb.InstanceConfig) | |
| policy.ToInstanceConfig(currentConfig) | |
| // Set the device policy based on new config. | |
| policyChanged := policy.SetFromInstanceConfig(config) | |
| if !policyChanged { | |
| glog.Infof("Skipping device policy update. Current: {%+v}", currentConfig) | |
| return nil | |
| } else { | |
| newConfig := new(pmpb.InstanceConfig) | |
| policy.ToInstanceConfig(newConfig) | |
| glog.Infof("Updating cos device policy. Instance config changed from {%+v} to {%+v}", | |
| currentConfig, newConfig) | |
| } | |
| return policy.SignAndWrite(privateKey, devicePolicyFile, instanceConfigFile) | |
| } | |
| func GetInstanceConfig(instanceConfigFile string) (config *pmpb.InstanceConfig, err error) { | |
| // Read the current device policy. | |
| policy := new(DevicePolicy) | |
| err = policy.ReadPolicyFromDisk(instanceConfigFile) | |
| if err != nil { | |
| return nil, err | |
| } | |
| config = new(pmpb.InstanceConfig) | |
| policy.ToInstanceConfig(config) | |
| return config, nil | |
| } |