Repository: micromdm/micromdm
Pull request #721 (2021-01-14 21:54:12)

Spruce up built in webpage
Used CSS from https://micromdm.io

williamtheaker: <img width="2032" alt="Screen Shot 2021-02-05 at 4 24 24 PM" src="https://user-images.githubusercontent.com/665387/107102219-ad19b880-67ce-11eb-92d5-6d05b35055ce.png">


Patch:
diff --git a/.circleci/config.yml b/.circleci/config.yml
deleted file mode 100644
index a09bd3a..0000000
--- a/.circleci/config.yml
+++ /dev/null
@@ -1,27 +0,0 @@
-version: 2
-jobs:
-  build-go1.11:
-    docker:
-        - image: golang:1.11
-        - image: postgres:9.6
-          environment:
-            POSTGRES_DB: micromdm
-            POSTGRES_USER: micromdm
-            POSTGRES_PASSWORD: micromdm
-
-    working_directory: /go/src/github.com/micromdm/micromdm
-    steps: &steps
-      - checkout
-      - run: apt-get update -y && apt-get install postgresql-client -y
-      - run: make deps
-      - run: GO111MODULE=on go install github.com/pressly/goose/cmd/goose
-      - run: make db-reset-test
-      - run: make db-migrate-test
-      - run: make test
-      - run: make
-
-workflows:
-  version: 2
-  build:
-    jobs:
-      - build-go1.11
diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml
new file mode 100644
index 0000000..362f1cf
--- /dev/null
+++ b/.github/workflows/CI.yml
@@ -0,0 +1,33 @@
+name: CI
+
+on:
+  push:
+    branches: [ main ]
+  pull_request:
+    types: [opened, reopened, synchronize]
+
+jobs:
+  build-test:
+    name: Build, test & format
+    strategy:
+      matrix:
+        go-version: [1.15.x, 1.16.x]
+        platform: [ubuntu-latest, macos-latest, windows-latest]
+    runs-on: ${{ matrix.platform }}
+    steps:
+    - uses: actions/checkout@v2
+
+    - name: setup go
+      uses: actions/setup-go@v2
+      with:
+        go-version: ${{ matrix.go-version }}
+
+    - name: Build
+      run: go build -v ./...
+
+    - name: Test
+      run: go test -v ./...
+
+    - name: Format
+      if: matrix.platform == 'ubuntu-latest'
+      run: if [ "$(gofmt -s -l . | wc -l)" -gt 0 ]; then exit 1; fi
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6b90ce0..35e1d95 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +1,6 @@
-## [v1.8.0](https://github.com/micromdm/micromdm/compare/v1.7.1...main) TBD
+## [Unreleased](https://github.com/micromdm/micromdm/compare/v1.8.0...main) TBD
+
+## [v1.8.0](https://github.com/micromdm/micromdm/compare/v1.7.1...v1.8.0) February, 2021
 
 - Fix embedded manifest of InstallEnterpriseApplication (#669)
 - Added Activation Lock Bypass support code (#677)
@@ -6,12 +8,19 @@
 - mdmctl can now have a base server URL (#683)
 - Fix an asymptomatic queue marshaling bug (#690)
 - Add ability to unassign DEP devices via API (#687)
+- A device's command queue is now cleared during enrollment (#692)
 - APNS is now proxy aware (#698)
 - Add `-validate-scep-issuer` and `-validate-scep-expiration` flags to only validate the SCEP certificate was issued by the MicrMDM SCEP CA, and optionally to validate that the certificate hasn't expired (#700)
 - Add `-udid-cert-auth-warn-only` flag that disables the UDID-certificate authentication mechanism. Can be used to help remediate [expiring device identity certificates](https://github.com/micromdm/micromdm/wiki/Device-Identity-Certificate-Expiration) (#643)
-- Fix for multiple InstallApplications in Blueprints (#643, #704)
+- Fix for multiple InstallApplications in Blueprints (#549, #704)
+- More secure argument passing in API scripts (#709)
+- TimeZone setting support in Settings command (#719)
+- Support tls-alpn-01 for Let's Encrypt certificates (#720)
+- Update MDM Vendor CSR signing to SHA-2 and use new Apple intermediate cert (#723, #725)
+- Avoid unnecessary command queue save/disk write (#711)
+- Documentation updates
 
-Thanks to our contributors for this release: @MobileDan, @meta-gitub, @grahamgilbert, @tperfitt
+Thanks to our contributors for this release: @MobileDan, @meta-gitub, @grahamgilbert, @tperfitt, @williamtheaker, @slawoslawo, @choehn-signogy, @natewalck, @korylprince
 
 ## [v1.7.1](https://github.com/micromdm/micromdm/compare/v1.7.0-alpha...v1.7.1) April, 2020
 
@@ -19,10 +28,11 @@ Thanks to our contributors for this release: @MobileDan, @meta-gitub, @grahamgil
 - Correctly handle DEP profile removal response (#666)
 - Fix permissions on API command tools (#667)
 - Fix TLS startup issue (#673)
+- Documentation updates
 
 Thanks to our contributors for this release: @bdemetris, @tricknotes, @netproteus
 
-## [v1.7.0](https://github.com/micromdm/micromdm/compare/v1.6.0...v1.7.0-alpha) (alpha) March, 2020
+## [v1.7.0-alpha](https://github.com/micromdm/micromdm/compare/v1.6.0...v1.7.0-alpha) March, 2020
 
 ### Reliability, scalability, security, and usability improvements:
 
diff --git a/Makefile b/Makefile
index 37e874b..79c3000 100644
--- a/Makefile
+++ b/Makefile
@@ -58,6 +58,10 @@ deps: gomodcheck
 	@go mod download
 
 test:
+	go test -cover ./...
+
+# don't run race tests by default. see https://github.com/etcd-io/bbolt/issues/187
+test-race:
 	go test -cover -race ./...
 
 build: micromdm mdmctl
diff --git a/README.md b/README.md
index 56b234a..74e9f34 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
 # MicroMDM - a devops friendly MDM server 
 
-[![CircleCI](https://circleci.com/gh/micromdm/micromdm/tree/main.svg?style=svg)](https://circleci.com/gh/micromdm/micromdm/tree/main)
+[![CI](https://github.com/micromdm/micromdm/workflows/CI/badge.svg)](https://github.com/micromdm/micromdm/actions)
 
 MicroMDM is a Mobile Device Management server for Apple Devices, focused on giving you all the power through an API. 
 
diff --git a/cmd/mdmctl/mdmcert.go b/cmd/mdmctl/mdmcert.go
index be00091..b806805 100644
--- a/cmd/mdmctl/mdmcert.go
+++ b/cmd/mdmctl/mdmcert.go
@@ -164,12 +164,12 @@ func (cmd *mdmcertCommand) runPush(args []string) error {
 	flagset := flag.NewFlagSet("push", flag.ExitOnError)
 	flagset.Usage = usageFor(flagset, "mdmctl mdmcert push [flags]")
 	var (
-		flEmail    = flagset.String("email", "", "Email address to use in CSR Subject.")
-		flCountry  = flagset.String("country", "US", "Two letter country code for the CSR Subject(Example: US).")
-		flCN       = flagset.String("cn", "micromdm-user", "CommonName for the CSR Subject.")
-		flPKeyPass = flagset.String("password", "", "Password to encrypt/read the RSA key.")
-		flKeyPath  = flagset.String("private-key", filepath.Join(mdmcertdir, pushCertificatePrivateKeyFilename), "Path to the push certificate private key. A new RSA key will be created at this path.")
-		flLocalOnly= flagset.Bool("local-only",false,"No server configuration required.")
+		flEmail     = flagset.String("email", "", "Email address to use in CSR Subject.")
+		flCountry   = flagset.String("country", "US", "Two letter country code for the CSR Subject(Example: US).")
+		flCN        = flagset.String("cn", "micromdm-user", "CommonName for the CSR Subject.")
+		flPKeyPass  = flagset.String("password", "", "Password to encrypt/read the RSA key.")
+		flKeyPath   = flagset.String("private-key", filepath.Join(mdmcertdir, pushCertificatePrivateKeyFilename), "Path to the push certificate private key. A new RSA key will be created at this path.")
+		flLocalOnly = flagset.Bool("local-only", false, "No server configuration required.")
 
 		flCSRPath = flagset.String("out", filepath.Join(mdmcertdir, pushCSRFilename), "Path to save the MDM Push Certificate request.")
 	)
@@ -178,10 +178,10 @@ func (cmd *mdmcertCommand) runPush(args []string) error {
 		return err
 	}
 
-    if !*flLocalOnly {
-        if err := cmd.setup(); err != nil {
-    		return err
-   		}
+	if !*flLocalOnly {
+		if err := cmd.setup(); err != nil {
+			return err
+		}
 	}
 	if err := os.MkdirAll(filepath.Dir(*flCSRPath), 0755); err != nil {
 		errors.Wrapf(err, "create directory %s", filepath.Dir(*flCSRPath))
diff --git a/cmd/micromdm/serve.go b/cmd/micromdm/serve.go
index f2c6695..cf3b7a4 100644
--- a/cmd/micromdm/serve.go
+++ b/cmd/micromdm/serve.go
@@ -54,15 +54,46 @@ const homePage = `<!doctype html>
 	<meta charset="utf-8">
 	<meta name="viewport" content="width=device-width, initial-scale=1">
 	<title>MicroMDM</title>
-	<style>
-		body {
-			font-family: -apple-system, BlinkMacSystemFont, sans-serif;
-		}
-	</style>
-</head>
+  <style>
+@font-face {
+  font-family: system;
+  font-weight: 900;
+  src: local(".SFNSText-Heavy"), local(".HelveticaNeueDeskInterface-Heavy"), local(".LucidaGrandeUI"), local("Ubuntu Heavy"), local("Segoe UI Heavy"), local("Roboto-Heavy"), local("DroidSans"), local("Tahoma");
+}
+
+/* Global */
+body {
+  position: relative;
+  font-family: "system", "Helvetica Neue", "Helvetica", "Arial", sans-serif;
+  margin: 0;
+  text-align: center;
+  font-size: 35px;
+}
+
+.enrollment {
+  background: #4a9dff;
+  color: #fff;
+  padding: 15px;
+  border-radius: 3px;
+  font-size: 30px;
+  text-decoration: none;
+}
+  </style>
+ </head>
 <body>
-	<h3>Welcome to MicroMDM!</h3>
-	<p><a href="mdm/enroll">Enroll a device</a></p>
+
+<h1>MicroMDM</h1>
+<svg xmlns="http://www.w3.org/2000/svg" width="204" height="192" viewBox="0 0 51 48">
+  <g fill="none" fill-rule="evenodd">
+      <path fill="#366BE0" d="M34 38L0 20 34 0z"/>
+      <path fill="#4A9DFF" d="M17 10l34 18-34 20z"/>
+      <path fill="#80CFFF" d="M17 29L0 39l17 9zM51 28L34 38l17 9z"/>
+  </g>
+</svg>
+
+
+<p><a class=enrollment href="mdm/enroll">Enroll a device</a></p>
+
 </body>
 </html>
 `
diff --git a/docs/user-guide/quickstart.md b/docs/user-guide/quickstart.md
index de395b6..7c5fed0 100644
--- a/docs/user-guide/quickstart.md
+++ b/docs/user-guide/quickstart.md
@@ -66,7 +66,7 @@ You're likely to run more than one instance of micromdm(ex: production and stagi
 
 To communicate with your device fleet, MDM needs an APNS certificate issued by Apple. As noted in the introduction, this process requires that you have an Enterprise Developer Account, and the `MDM CSR` option enabled under the *Certificates, IDs & Profiles* tab for *iOS*. 
 
-Apple has a separate flow for the MDM vendor than the one for customers. For an in-house deployment without third parties, you must complete boththe vendor and the customer process yourself. The `mdmctl mdmcert` command will help you with your APNS certificate needs.
+Apple has a separate flow for the MDM vendor than the one for customers. For an in-house deployment without third parties, you must complete both the vendor and the customer process yourself. The `mdmctl mdmcert` command will help you with your APNS certificate needs.
 
 Create a request for the MDM CSR, with a password used to encrypt the private key. 
 After this step you will have a new `mdm-certificates` directory, with the necessary files. 
diff --git a/mdm/mdm/command.go b/mdm/mdm/command.go
index e10ad4c..16e2b81 100644
--- a/mdm/mdm/command.go
+++ b/mdm/mdm/command.go
@@ -217,7 +217,7 @@ type Setting struct {
 	DeviceName              *string                `plist:",omitempty" json:"device_name,omitempty"`
 	HostName                *string                `plist:",omitempty" json:"hostname,omitempty"`
 	Identifier              *string                `plist:",omitempty" json:"identifier"`
-	TimeZone              	*string                `plist:",omitempty" json:"time_zone,omitempty"`
+	TimeZone                *string                `plist:",omitempty" json:"time_zone,omitempty"`
 	Attributes              map[string]string      `plist:",omitempty" json:"attributes,omitempty"`
 	Image                   []byte                 `plist:",omitempty" json:"image,omitempty"`
 	Where                   *int                   `plist:",omitempty" json:"where,omitempty"`
@@ -243,12 +243,12 @@ type ManagedApplicationFeedback struct {
 
 type SetFirmwarePassword struct {
 	CurrentPassword string `plist:",omitempty" json:"current_password,omitempty"`
-	NewPassword     string `plist:",omitempty" json:"new_password,omitempty"`
+	NewPassword     string `json:"new_password"`
 	AllowOroms      bool   `plist:",omitempty" json:"allow_oroms,omitempty"`
 }
 
 type VerifyFirmwarePassword struct {
-	Password string `plist:",omitempty" json:"password,omitempty"`
+	Password string `json:"password"`
 }
 
 type SetAutoAdminPassword struct {
diff --git a/mdm/mdm/marshal_proto.go b/mdm/mdm/marshal_proto.go
index 8c207fd..bb133cc 100644
--- a/mdm/mdm/marshal_proto.go
+++ b/mdm/mdm/marshal_proto.go
@@ -441,7 +441,7 @@ func settingToProto(s Setting) *mdmproto.Setting {
 		pbs.TimeZone = &mdmproto.TimeZoneSetting{
 			TimeZone: emptyStringIfNil(s.TimeZone),
 		}
-		case "HostName":
+	case "HostName":
 		pbs.Hostname = &mdmproto.HostnameSetting{
 			Hostname: emptyStringIfNil(s.HostName),
 		}
diff --git a/pkg/crypto/mdmcertutil/certutil.go b/pkg/crypto/mdmcertutil/certutil.go
index 79c3180..ac834e4 100644
--- a/pkg/crypto/mdmcertutil/certutil.go
+++ b/pkg/crypto/mdmcertutil/certutil.go
@@ -7,7 +7,7 @@ import (
 	"crypto"
 	"crypto/rand"
 	"crypto/rsa"
-	"crypto/sha1"
+	"crypto/sha256"
 	"crypto/x509"
 	"crypto/x509/pkix"
 	"encoding/base64"
@@ -72,7 +72,7 @@ func (p *PushCertificateRequest) Encode() ([]byte, error) {
 }
 
 const (
-	wwdrIntermediaryURL = "https://developer.apple.com/certificationauthority/AppleWWDRCA.cer"
+	wwdrIntermediaryURL = "https://www.apple.com/certificateauthority/AppleWWDRCAG3.cer"
 	appleRootCAURL      = "http://www.apple.com/appleca/AppleIncRootCertificate.cer"
 )
 
@@ -134,9 +134,9 @@ func makeCertChain(mdmPEM, wwdrPEM, rootPEM []byte) string {
 }
 
 func signPushCSR(csrData []byte, key *rsa.PrivateKey) ([]byte, error) {
-	h := sha1.New()
+	h := sha256.New()
 	h.Write(csrData)
-	signature, err := rsa.SignPKCS1v15(rand.Reader, key, crypto.SHA1, h.Sum(nil))
+	signature, err := rsa.SignPKCS1v15(rand.Reader, key, crypto.SHA256, h.Sum(nil))
 	return signature, errors.Wrap(err, "signing push CSR")
 }
 
diff --git a/platform/apns/service.go b/platform/apns/service.go
index 13134c6..01e4872 100644
--- a/platform/apns/service.go
+++ b/platform/apns/service.go
@@ -136,7 +136,7 @@ func newClient(cert tls.Certificate) (*http.Client, error) {
 	}
 	config.BuildNameToCertificate()
 	transport := &http.Transport{
-		Proxy: http.ProxyFromEnvironment,
+		Proxy:           http.ProxyFromEnvironment,
 		TLSClientConfig: config,
 		IdleConnTimeout: 90 * time.Second,
 	}
diff --git a/platform/blueprint/blueprint.go b/platform/blueprint/blueprint.go
index c008817..a309be8 100644
--- a/platform/blueprint/blueprint.go
+++ b/platform/blueprint/blueprint.go
@@ -43,7 +43,7 @@ func MarshalBlueprint(bp *Blueprint) ([]byte, error) {
 		UserUuid:                            bp.UserUUID,
 		SkipPrimarySetupAccountCreation:     bp.SkipPrimarySetupAccountCreation,
 		SetPrimarySetupAccountAsRegularUser: bp.SetPrimarySetupAccountAsRegularUser,
-		ApplyAt: bp.ApplyAt,
+		ApplyAt:                             bp.ApplyAt,
 	}
 	return proto.Marshal(&protobp)
 }
diff --git a/platform/queue/queue.go b/platform/queue/queue.go
index 298bdc1..f9d4e78 100644
--- a/platform/queue/queue.go
+++ b/platform/queue/queue.go
@@ -164,8 +164,12 @@ func (db *Store) nextCommand(ctx context.Context, resp mdm.Response) (*Command,
 		}
 	}
 
-	if err := db.Save(dc); err != nil {
-		return nil, err
+	// we only need to Save if there are command queue changes such as
+	// NowNow and Acknowledged responses or a new popped command.
+	if resp.Status != "Idle" || cmd != nil {
+		if err := db.Save(dc); err != nil {
+			return nil, err
+		}
 	}
 
 	return cmd, nil
diff --git a/tools/ngrok/README.md b/tools/ngrok/README.md
index 52a7798..5ac6c14 100644
--- a/tools/ngrok/README.md
+++ b/tools/ngrok/README.md
@@ -28,7 +28,7 @@ Some of the steps are only required for first time setup.
 
 _Note:_ When the ngrok tunnel expires, you have to re-enroll the test device. The `configure_mdmctl` updates the DEP profile, so all you need to do is either reset the VM, or run `sudo /usr/bin/profiles -e -N` if testing with a real Mac.
 
-1. Run ngrok. A tunnel will be opened. If you're using the free version of `ngrok` this URL is only valid for 8 hours, but you can always start with a new URL.
+1. Run ngrok. A tunnel will be opened. If you're using the free version of `ngrok` this URL is only valid for 2 hours, but you can always start with a new URL.
 ```
 ngrok http 8080
 ```


Test patch:
diff --git a/mdm/mdm/mdm_command_test.go b/mdm/mdm/mdm_command_test.go
index 083711b..84a3efb 100644
--- a/mdm/mdm/mdm_command_test.go
+++ b/mdm/mdm/mdm_command_test.go
@@ -62,6 +62,30 @@ func TestMarshalCommand(t *testing.T) {
 				RequestType: "DisableRemoteDesktop",
 			},
 		},
+		{
+			Command: Command{
+				RequestType: "SetFirmwarePassword",
+				SetFirmwarePassword: &SetFirmwarePassword{
+					CurrentPassword: "test",
+				},
+			},
+		},
+		{
+			Command: Command{
+				RequestType: "SetFirmwarePassword",
+				SetFirmwarePassword: &SetFirmwarePassword{
+					NewPassword: "test",
+				},
+			},
+		},
+		{
+			Command: Command{
+				RequestType: "SetFirmwarePassword",
+				VerifyFirmwarePassword: &VerifyFirmwarePassword{
+					Password: "test",
+				},
+			},
+		},
 	}
 	for _, tt := range tests {
 		t.Run(tt.Command.RequestType+"_json", func(t *testing.T) {
@@ -223,6 +247,60 @@ func TestEndToEnd(t *testing.T) {
 				}
 			},
 		},
+
+		{
+			name: "SetFirmwarePassword_NoNewPassword",
+			requestBytes: []byte(
+				`{"request_type":"SetFirmwarePassword","current_password":"test"}`,
+			),
+			testFn: func(t *testing.T, parts endToEndParts) {
+				needToSee := [][]byte{
+					[]byte(`CurrentPassword`),
+					[]byte(`test`),
+					[]byte(`NewPassword`),
+				}
+				for _, b := range needToSee {
+					if !bytes.Contains(parts.plistData, b) {
+						t.Error(fmt.Sprintf("marshaled plist does not contain required bytes: '%s'", string(b)))
+					}
+				}
+			},
+		},
+		{
+			name: "SetFirmwarePassword_NewPassword",
+			requestBytes: []byte(
+				`{"request_type":"SetFirmwarePassword","new_password":"test"}`,
+			),
+			testFn: func(t *testing.T, parts endToEndParts) {
+				needToSee := [][]byte{
+					[]byte(`NewPassword`),
+					[]byte(`test`),
+				}
+				for _, b := range needToSee {
+					if !bytes.Contains(parts.plistData, b) {
+						t.Error(fmt.Sprintf("marshaled plist does not contain required bytes: '%s'", string(b)))
+					}
+				}
+			},
+		},
+
+		{
+			name: "VerifyFirmwarePassword",
+			requestBytes: []byte(
+				`{"request_type":"VerifyFirmwarePassword","password":"test"}`,
+			),
+			testFn: func(t *testing.T, parts endToEndParts) {
+				needToSee := [][]byte{
+					[]byte(`Password`),
+					[]byte(`test`),
+				}
+				for _, b := range needToSee {
+					if !bytes.Contains(parts.plistData, b) {
+						t.Error(fmt.Sprintf("marshaled plist does not contain required bytes: '%s'", string(b)))
+					}
+				}
+			},
+		},
 	}
 
 	for _, tt := range tests {
diff --git a/platform/apns/pg/pg_test.go b/platform/apns/pg/pg_test.go
index a7e1e1d..0a90610 100644
--- a/platform/apns/pg/pg_test.go
+++ b/platform/apns/pg/pg_test.go
@@ -1,3 +1,5 @@
+// +build pg
+
 package pg
 
 import (
diff --git a/platform/device/pg/pg_test.go b/platform/device/pg/pg_test.go
index aa45f41..5abef45 100644
--- a/platform/device/pg/pg_test.go
+++ b/platform/device/pg/pg_test.go
@@ -1,3 +1,5 @@
+// +build pg
+
 package pg
 
 import (