From 26bc02d6d353c3d0ef1cfffcdebc5d6105015c39 Mon Sep 17 00:00:00 2001 From: Jesse Peterson Date: Fri, 16 Jun 2023 09:34:23 -0700 Subject: [PATCH 1/2] Support for generating MAID Access Management JWTs --- cmd/depserver/main.go | 6 +++ docs/openapi.yaml | 21 ++++++++++ docs/operations-guide.md | 17 ++++++++ go.mod | 2 + go.sum | 4 ++ http/api/maid.go | 88 ++++++++++++++++++++++++++++++++++++++++ http/api/maid_test.go | 33 +++++++++++++++ tools/cfg-maidjwt.sh | 8 ++++ 8 files changed, 179 insertions(+) create mode 100644 http/api/maid.go create mode 100644 http/api/maid_test.go create mode 100755 tools/cfg-maidjwt.sh diff --git a/cmd/depserver/main.go b/cmd/depserver/main.go index 985d03a..e2801e2 100644 --- a/cmd/depserver/main.go +++ b/cmd/depserver/main.go @@ -28,6 +28,7 @@ const ( endpointConfig = "/v1/config/" endpointTokenPKI = "/v1/tokenpki/" endpointAssigner = "/v1/assigner/" + endpointMAIDJWT = "/v1/maidjwt/" endpointProxy = "/proxy/" ) @@ -91,6 +92,11 @@ func main() { assignerMux.Handle("PUT", api.StoreAssignerProfileHandler(storage, logger.With("handler", "store-assigner-profile"))) handleStrippedAPI(assignerMux, endpointAssigner) + handleStrippedAPI( + api.MAIDJWTHandler(storage, logger.With("handler", "get-maid-jwt")), + endpointMAIDJWT, + ) + p := proxy.New( client.NewTransport(http.DefaultTransport, http.DefaultClient, storage, nil), storage, diff --git a/docs/openapi.yaml b/docs/openapi.yaml index 7e7a2b5..1bb8f4f 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -246,6 +246,27 @@ paths: $ref: '#/components/responses/JSONAPIError' parameters: - $ref: '#/components/parameters/depName' + /v1/maidjwt/{name}: + get: + description: Generate Managed Apple ID Managed Access JWT. This JWT is for use in response to a device's MDM `GetToken` Check-in message with a `TokenServiceType` of `com.apple.maid`. Note this endpoint queries the DEP Account Details endpoint to retrieve the Server UUID. + security: + - basicAuth: [] + responses: + '200': + description: JWT which includes a claim containing the DEP server UUID and is signed by the private key of the DEP OAuth tokens. + content: + application/jwt: + schema: + type: string + example: eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE2ODY5MzE5NzIsImlzcyI6IjdERDczOUEyQTQ3MjRBMzI4MkFBODY0RUJBRDMwRkZGIiwianRpIjoiYmJiOGFkYWYtMDRiZS00NWNiLWFlMjgtNWI2NTc4NTdkZDg4Iiwic2VydmljZV90eXBlIjoiY29tLmFwcGxlLm1haWQifQ.IhPEK_jgJe3hxSyzBNYxGyqgeqmiT24MFJa5z6GQa-0vJ1FyEah8uG2ui9bfFKBM7HfbY122pKjEBbsv-oYMiRm9kOWvLb-GzDIb0WCx2c12al5OgIyR6c0cGINVALIDhm7vchMu-MrRS6TYp79FcXUcxHe8i2JxPlKJA9I4De4fCHZb4pgbMVpXbAklglCl7REwnP62BGEJUjSyqYNJTHTsgTFkEmLUgBulw99Yz1tXcJEgu5vnKtEWINVALIDcEyK9e6ek4GhfkzeEHd6WlML0wvZiiFG_8XUI4X3a4GfaAXHwTWHbOxyOGbD5GjhE1tPkT9tt5u6w + '401': + $ref: '#/components/responses/UnauthorizedError' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/JSONAPIError' + parameters: + - $ref: '#/components/parameters/depName' components: parameters: depName: diff --git a/docs/operations-guide.md b/docs/operations-guide.md index 5af2313..f233c80 100644 --- a/docs/operations-guide.md +++ b/docs/operations-guide.md @@ -104,6 +104,12 @@ The `/v1/assigner/{name}` endpoints deal with storing and retrieving the assigne The `/v1/config/{name}` endpoints deal with storing and retrieving configuration for a given DEP name. At this time the only configuration available is the base URL of the DEP name. This is really only useful when talking to the DEP simulator `depsim` or perhaps directing DEP server requests through another reverse proxy. +#### MAID JWT + +* Endpoint: `GET /v1/maidjwt/{name}` + +The `/v1/maidjwt/{name}` endpoint generates a JWT for Managed Apple ID Access Management. The responses is intended for a device's MDM `GetToken` Check-in message with a `TokenServiceType` of `com.apple.maid`. Note this endpoint queries the DEP Account Details endpoint to retrieve the Server UUID so be aware of the frequency it is called. + ### Reverse proxy In addition to individually handling some of various Apple DEP API endpoints in its `godep` library NanoDEP provides a transparently-authenticating HTTP reverse proxy to the Apple DEP servers. This allows us to simply provide `depserver` with the Apple DEP endpoint, the NanoDEP "DEP name" and the API key, and we can talk to any of the Apple DEP endpoint APIs (including the Roster, Class, and People Management). `depserver` will authenticate to the Apple DEP server and keep track of session management transparently behind the scenes. To be clear: this means you do not have to call to the `/session` endpoint to authenticate nor to manage and update the session tokens with each request. NanoDEP does this for you. @@ -338,6 +344,17 @@ $ ./dep-remove-profile.sh 07AAD449616F566C12 } ``` +#### cfg-maidjwt.sh + +For the DEP "MDM server" in the environment variable $DEP_NAME (see above) this script queries the DEP API and generates a JWT. This is intended as a response to a device's MDM `GetToken` Check-in message with a `TokenServiceType` of `com.apple.maid` + +##### Example usage + +```bash +$ ./tools/cfg-maidjwt.sh +eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE2ODY5MzE5NzIsImlzcyI6IjdERDczOUEyQTQ3MjRBMzI4MkFBODY0RUJBRDMwRkZGIiwianRpIjoiYmJiOGFkYWYtMDRiZS00NWNiLWFlMjgtNWI2NTc4NTdkZDg4Iiwic2VydmljZV90eXBlIjoiY29tLmFwcGxlLm1haWQifQ.IhPEK_jgJe3hxSyzBNYxGyqgeqmiT24MFJa5z6GQa-0vJ1FyEah8uG2ui9bfFKBM7HfbY122pKjEBbsv-oYMiRm9kOWvLb-GzDIb0WCx2c12al5OgIyR6c0cGINVALIDhm7vchMu-MrRS6TYp79FcXUcxHe8i2JxPlKJA9I4De4fCHZb4pgbMVpXbAklglCl7REwnP62BGEJUjSyqYNJTHTsgTFkEmLUgBulw99Yz1tXcJEgu5vnKtEWINVALIDcEyK9e6ek4GhfkzeEHd6WlML0wvZiiFG_8XUI4X3a4GfaAXHwTWHbOxyOGbD5GjhE1tPkT9tt5u6w +``` + ### Troubleshooting Sometimes something goes wrong with the API or the scripts. Sometimes the API will tell you exactly the problem you have and how you can fix the input. Other times you may need to inspect the HTTP request details. To do that you can turn on `curl` verbose output by setting the `CURL_OPTS` environment variable which all the scripts utilize: diff --git a/go.mod b/go.mod index e5f47bd..0681602 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,9 @@ go 1.17 require ( github.com/go-sql-driver/mysql v1.8.1 + github.com/golang-jwt/jwt/v5 v5.0.0 github.com/gomodule/oauth1 v0.2.0 + github.com/google/uuid v1.3.0 github.com/smallstep/pkcs7 v0.0.0-20231107075624-be1870d87d13 ) diff --git a/go.sum b/go.sum index dd64ff9..59a33fb 100644 --- a/go.sum +++ b/go.sum @@ -2,7 +2,11 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= +github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE= +github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/gomodule/oauth1 v0.2.0 h1:/nNHAD99yipOEspQFbAnNmwGTZ1UNXiD/+JLxwx79fo= github.com/gomodule/oauth1 v0.2.0/go.mod h1:4r/a8/3RkhMBxJQWL5qzbOEcaQmNPIkNoI7P8sXeI08= github.com/smallstep/pkcs7 v0.0.0-20231107075624-be1870d87d13 h1:qRxEt9ESQhAg1kjmgJ8oyyzlc9zkAjOooe7bcKjKORQ= github.com/smallstep/pkcs7 v0.0.0-20231107075624-be1870d87d13/go.mod h1:SoUAr/4M46rZ3WaLstHxGhLEgoYIDRqxQEXLOmOEB0Y= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= diff --git a/http/api/maid.go b/http/api/maid.go new file mode 100644 index 0000000..aec8576 --- /dev/null +++ b/http/api/maid.go @@ -0,0 +1,88 @@ +package api + +import ( + "encoding/json" + "net/http" + "os" + "time" + + "github.com/golang-jwt/jwt/v5" + "github.com/google/uuid" + "github.com/micromdm/nanodep/godep" + "github.com/micromdm/nanodep/log" + "github.com/micromdm/nanodep/log/ctxlog" + "github.com/micromdm/nanodep/tokenpki" +) + +const maidJWTserviceType = "com.apple.maid" + +func newMAIDCheckinJWT(depUUID string, key interface{}) (string, error) { + tok := jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.MapClaims{ + "iss": depUUID, + "iat": time.Now().Unix(), + "jti": uuid.NewString(), + "service_type": maidJWTserviceType, + }) + return tok.SignedString(key) +} + +type MAIDJWTStorage interface { + TokenPKIRetriever + godep.ClientStorage +} + +// MAIDJWTHandler returns a JWT for DEP Access Management. This JWT should +// be returned to an MDM client's CheckIn "GetToken" message. Note: +// this queries the DEP API "live." A cache of some sort may be a future +// strategy. +func MAIDJWTHandler(store MAIDJWTStorage, logger log.Logger) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + logger := ctxlog.Logger(r.Context(), logger) + if r.URL.Path == "" { + logger.Info("msg", "DEP name check", "err", "missing DEP name") + http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) + return + } + + name := r.URL.Path + logger = logger.With("name", name) + + client := godep.NewClient(store, nil) + detail, err := client.AccountDetail(r.Context(), name) + if err != nil { + logger.Info("msg", "getting account detail", "err", err) + jsonError(w, err) + return + } + + json.NewEncoder(os.Stdout).Encode(detail) + + _, keyBytes, err := store.RetrieveTokenPKI(r.Context(), name) + if err != nil { + logger.Info("msg", "retrieving token keypair", "err", err) + jsonError(w, err) + return + } + + key, err := tokenpki.RSAKeyFromPEM(keyBytes) + if err != nil { + logger.Info("msg", "decoding retrieved private key", "err", err) + jsonError(w, err) + return + } + + jwt, err := newMAIDCheckinJWT(detail.ServerUUID, key) + if err != nil { + logger.Info("msg", "creating MAID JWT", "err", err) + jsonError(w, err) + return + } + + w.Header().Set("Content-type", "application/jwt") + _, err = w.Write([]byte(jwt)) + if err != nil { + logger.Info("msg", "writing response body", "err", err) + return + } + } +} diff --git a/http/api/maid_test.go b/http/api/maid_test.go new file mode 100644 index 0000000..7babd42 --- /dev/null +++ b/http/api/maid_test.go @@ -0,0 +1,33 @@ +package api + +import ( + "crypto/rand" + "crypto/rsa" + "testing" + + "github.com/golang-jwt/jwt/v5" + "github.com/google/uuid" +) + +func TestMAIDCheckinJWT(t *testing.T) { + privKey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + t.Fatal(err) + } + u := uuid.NewString() + s, err := newMAIDCheckinJWT(u, privKey) + if err != nil { + t.Fatal(err) + } + tok, err := jwt.Parse(s, func(_ *jwt.Token) (interface{}, error) { return &privKey.PublicKey, nil }) + if err != nil { + t.Fatal(err) + } + iss, err := tok.Claims.GetIssuer() + if err != nil { + t.Fatal(err) + } + if have, want := iss, u; have != want { + t.Errorf("have %v, want %v", have, want) + } +} diff --git a/tools/cfg-maidjwt.sh b/tools/cfg-maidjwt.sh new file mode 100755 index 0000000..4f4025c --- /dev/null +++ b/tools/cfg-maidjwt.sh @@ -0,0 +1,8 @@ +#!/bin/sh + +URL="${BASE_URL}/v1/maidjwt/${DEP_NAME}" + +curl \ + $CURL_OPTS \ + -u "depserver:$APIKEY" \ + "$URL" From 401faf4f8592797a0d9db39eb4b42dfe732ede1c Mon Sep 17 00:00:00 2001 From: Jesse Peterson Date: Fri, 16 Jun 2023 09:37:57 -0700 Subject: [PATCH 2/2] update go.mod to Go 1.18 --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 0681602..caa8357 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/micromdm/nanodep -go 1.17 +go 1.18 require ( github.com/go-sql-driver/mysql v1.8.1