Merge "Allow Device ID attestation profile owner"
diff --git a/keystore-engine/Android.bp b/keystore-engine/Android.bp
new file mode 100644
index 0000000..e7cd09d
--- /dev/null
+++ b/keystore-engine/Android.bp
@@ -0,0 +1,74 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// 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.
+
+cc_library_shared {
+ name: "libkeystore-engine",
+
+ srcs: [
+ "android_engine.cpp",
+ "keystore_backend_binder.cpp",
+ ],
+
+ cflags: [
+ "-fvisibility=hidden",
+ "-Wall",
+ "-Werror",
+ ],
+ cpp_std: "c++17",
+
+ shared_libs: [
+ "libbinder",
+ "libcrypto",
+ "libcutils",
+ "libhidlbase",
+ "libkeystore_aidl",
+ "libkeystore_binder",
+ "libkeystore_parcelables",
+ "liblog",
+ "libbase",
+ "libutils",
+ ],
+
+}
+
+// This builds a variant of libkeystore-engine that uses a HIDL HAL
+// owned by the WiFi user to perform signing operations.
+cc_library_shared {
+ name: "libkeystore-engine-wifi-hidl",
+
+ srcs: [
+ "android_engine.cpp",
+ "keystore_backend_hidl.cpp",
+ ],
+
+ cflags: [
+ "-fvisibility=hidden",
+ "-Wall",
+ "-Werror",
+ "-DBACKEND_WIFI_HIDL",
+ ],
+ cpp_std: "c++17",
+
+ shared_libs: [
+ "android.system.wifi.keystore@1.0",
+ "libcrypto",
+ "liblog",
+ "libhidlbase",
+ "libhidltransport",
+ "libcutils",
+ "libutils",
+ ],
+
+ vendor: true,
+}
diff --git a/keystore-engine/Android.mk b/keystore-engine/Android.mk
deleted file mode 100644
index e182dbd..0000000
--- a/keystore-engine/Android.mk
+++ /dev/null
@@ -1,71 +0,0 @@
-# Copyright (C) 2012 The Android Open Source Project
-#
-# 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.
-
-LOCAL_PATH := $(call my-dir)
-
-include $(CLEAR_VARS)
-
-LOCAL_MODULE := libkeystore-engine
-
-LOCAL_SRC_FILES := \
- android_engine.cpp \
- keystore_backend_binder.cpp
-
-LOCAL_MODULE_TAGS := optional
-LOCAL_CFLAGS := -fvisibility=hidden -Wall -Werror
-LOCAL_CPPFLAGS += -std=c++17
-
-LOCAL_SHARED_LIBRARIES += \
- libbinder \
- libcrypto \
- libcutils \
- libhidlbase \
- libkeystore_aidl \
- libkeystore_binder \
- libkeystore_parcelables \
- liblog \
- libbase \
- libutils
-
-LOCAL_ADDITIONAL_DEPENDENCIES := $(LOCAL_PATH)/Android.mk
-
-include $(BUILD_SHARED_LIBRARY)
-
-include $(CLEAR_VARS)
-
-# This builds a variant of libkeystore-engine that uses a HIDL HAL
-# owned by the WiFi user to perform signing operations.
-LOCAL_MODULE := libkeystore-engine-wifi-hidl
-
-LOCAL_SRC_FILES := \
- android_engine.cpp \
- keystore_backend_hidl.cpp
-
-LOCAL_MODULE_TAGS := optional
-LOCAL_CFLAGS := -fvisibility=hidden -Wall -Werror -DBACKEND_WIFI_HIDL
-LOCAL_CPPFLAGS += -std=c++17
-
-LOCAL_SHARED_LIBRARIES += \
- android.system.wifi.keystore@1.0 \
- libcrypto \
- liblog \
- libhidlbase \
- libhidltransport \
- libcutils \
- libutils
-
-LOCAL_ADDITIONAL_DEPENDENCIES := $(LOCAL_PATH)/Android.mk
-LOCAL_VENDOR_MODULE := true
-
-include $(BUILD_SHARED_LIBRARY)
diff --git a/keystore-engine/keystore_backend_binder.cpp b/keystore-engine/keystore_backend_binder.cpp
index 0e38b50..f6ae7f5 100644
--- a/keystore-engine/keystore_backend_binder.cpp
+++ b/keystore-engine/keystore_backend_binder.cpp
@@ -25,6 +25,7 @@
#include <android-base/logging.h>
#include <android/security/keystore/IKeystoreService.h>
#include <binder/IServiceManager.h>
+#include <binder/ProcessState.h>
#include <keystore/KeyCharacteristics.h>
#include <keystore/KeymasterArguments.h>
#include <keystore/KeymasterBlob.h>
@@ -37,6 +38,7 @@
#include <keystore/keystore_return_types.h>
#include <future>
+#include <thread>
using android::security::keystore::IKeystoreService;
using namespace android;
@@ -87,6 +89,10 @@
return {};
}
+KeystoreBackendBinder::KeystoreBackendBinder() {
+ android::ProcessState::self()->startThreadPool();
+}
+
int32_t KeystoreBackendBinder::sign(const char* key_id, const uint8_t* in, size_t len,
uint8_t** reply, size_t* reply_len) {
sp<IServiceManager> sm = defaultServiceManager();
diff --git a/keystore-engine/keystore_backend_binder.h b/keystore-engine/keystore_backend_binder.h
index 1db90f7..4c828c5 100644
--- a/keystore-engine/keystore_backend_binder.h
+++ b/keystore-engine/keystore_backend_binder.h
@@ -27,7 +27,7 @@
class KeystoreBackendBinder : public KeystoreBackend {
public:
- KeystoreBackendBinder() {}
+ KeystoreBackendBinder();
virtual ~KeystoreBackendBinder() {}
int32_t sign(const char *key_id, const uint8_t* in, size_t len,
uint8_t** reply, size_t* reply_len) override;
diff --git a/keystore/KeyStore.cpp b/keystore/KeyStore.cpp
index 7530243..ac3ab5f 100644
--- a/keystore/KeyStore.cpp
+++ b/keystore/KeyStore.cpp
@@ -138,21 +138,13 @@
android::String8 prefix("");
android::Vector<android::String16> aliases;
- // DO NOT
- // move
- // auto userState = userStateDB_.getUserState(userId);
- // here, in an attempt to replace userStateDB_.getUserState(userId) with userState.
- // userState is a proxy that holds a lock which may required by a worker.
- // LockedKeyBlobEntry::list has a fence that waits until all workers have finished which may
- // not happen if a user state lock is held. The following line only briefly grabs the lock.
- // Grabbing the user state lock after the list call is also save since workers cannot grab
- // blob entry locks.
-
auto userState = mUserStateDB.getUserState(userId);
std::string userDirName = userState->getUserDirName();
auto encryptionKey = userState->getEncryptionKey();
auto state = userState->getState();
- // unlock the user state
+ // userState is a proxy that holds a lock which may be required by a worker.
+ // LockedKeyBlobEntry::list has a fence that waits until all workers have finished which may
+ // not happen if a user state lock is held. The following line relinquishes the lock.
userState = {};
ResponseCode rc;
@@ -217,7 +209,7 @@
bool KeyStore::isEmpty(uid_t userId) const {
std::string userDirName;
{
- // userState hold a lock which must be relinqhished before list is called. This scope
+ // userState holds a lock which must be relinquished before list is called. This scope
// prevents deadlocks.
auto userState = mUserStateDB.getUserState(userId);
if (!userState) {
diff --git a/keystore/binder/android/security/keystore/IKeystoreService.aidl b/keystore/binder/android/security/keystore/IKeystoreService.aidl
index a760138..ea1e0f4 100644
--- a/keystore/binder/android/security/keystore/IKeystoreService.aidl
+++ b/keystore/binder/android/security/keystore/IKeystoreService.aidl
@@ -75,4 +75,5 @@
int cancelConfirmationPrompt(IBinder listener);
boolean isConfirmationPromptSupported();
int onKeyguardVisibilityChanged(in boolean isShowing, in int userId);
+ int listUidsOfAuthBoundKeys(out int[] uids);
}
diff --git a/keystore/blob.h b/keystore/blob.h
index 5cd1b90..a7f9fd0 100644
--- a/keystore/blob.h
+++ b/keystore/blob.h
@@ -196,10 +196,10 @@
bool hasCharacteristicsBlob() const;
bool operator<(const KeyBlobEntry& rhs) const {
- return std::tie(alias_, user_dir_, uid_) < std::tie(rhs.alias_, rhs.user_dir_, uid_);
+ return std::tie(uid_, alias_, user_dir_) < std::tie(rhs.uid_, rhs.alias_, rhs.user_dir_);
}
bool operator==(const KeyBlobEntry& rhs) const {
- return std::tie(alias_, user_dir_, uid_) == std::tie(rhs.alias_, rhs.user_dir_, uid_);
+ return std::tie(uid_, alias_, user_dir_) == std::tie(rhs.uid_, rhs.alias_, rhs.user_dir_);
}
bool operator!=(const KeyBlobEntry& rhs) const { return !(*this == rhs); }
diff --git a/keystore/key_store_service.cpp b/keystore/key_store_service.cpp
index 76db1d9..0039ea4 100644
--- a/keystore/key_store_service.cpp
+++ b/keystore/key_store_service.cpp
@@ -254,10 +254,10 @@
ResponseCode rc;
std::list<LockedKeyBlobEntry> internal_matches;
+ auto userDirName = mKeyStore->getUserStateDB().getUserStateByUid(targetUid)->getUserDirName();
- std::tie(rc, internal_matches) = LockedKeyBlobEntry::list(
- mKeyStore->getUserStateDB().getUserStateByUid(targetUid)->getUserDirName(),
- [&](uid_t uid, const std::string& alias) {
+ std::tie(rc, internal_matches) =
+ LockedKeyBlobEntry::list(userDirName, [&](uid_t uid, const std::string& alias) {
std::mismatch(stdPrefix.begin(), stdPrefix.end(), alias.begin(), alias.end());
return uid == static_cast<uid_t>(targetUid) &&
std::mismatch(stdPrefix.begin(), stdPrefix.end(), alias.begin(), alias.end())
@@ -274,6 +274,79 @@
return Status::ok();
}
+/*
+ * This method will return the uids of all auth bound keys for the calling user.
+ * This is intended to be used for alerting the user about which apps will be affected
+ * if the password/pin is removed. Only allowed to be called by system.
+ * The output is bound by the initial size of uidsOut to be compatible with Java.
+ */
+Status KeyStoreService::listUidsOfAuthBoundKeys(::std::vector<int32_t>* uidsOut,
+ int32_t* aidl_return) {
+ const int32_t callingUid = IPCThreadState::self()->getCallingUid();
+ const int32_t userId = get_user_id(callingUid);
+ const int32_t appId = get_app_id(callingUid);
+ if (appId != AID_SYSTEM) {
+ ALOGE("Permission listUidsOfAuthBoundKeys denied for aid %d", appId);
+ *aidl_return = static_cast<int32_t>(ResponseCode::PERMISSION_DENIED);
+ return Status::ok();
+ }
+
+ const String8 prefix8("");
+ auto userState = mKeyStore->getUserStateDB().getUserState(userId);
+ const std::string userDirName = userState->getUserDirName();
+ auto encryptionKey = userState->getEncryptionKey();
+ auto state = userState->getState();
+ // unlock the user state
+ userState = {};
+
+ ResponseCode rc;
+ std::list<LockedKeyBlobEntry> internal_matches;
+ std::tie(rc, internal_matches) =
+ LockedKeyBlobEntry::list(userDirName, [&](uid_t, const std::string&) {
+ // Need to filter on auth bound state, so just return true.
+ return true;
+ });
+ if (rc != ResponseCode::NO_ERROR) {
+ ALOGE("Error listing blob entries for user %d", userId);
+ return Status::fromServiceSpecificError(static_cast<int32_t>(rc));
+ }
+
+ auto it = uidsOut->begin();
+ for (LockedKeyBlobEntry& entry : internal_matches) {
+ if (it == uidsOut->end()) {
+ ALOGW("Maximum number (%d) of auth bound uids found, truncating remainder",
+ static_cast<int32_t>(uidsOut->capacity()));
+ break;
+ }
+ if (std::find(uidsOut->begin(), it, entry->uid()) != it) {
+ // uid already in list, skip
+ continue;
+ }
+
+ auto [rc, blob, charBlob] = entry.readBlobs(encryptionKey, state);
+ if (rc != ResponseCode::NO_ERROR && rc != ResponseCode::LOCKED) {
+ ALOGE("Error reading blob for key %s", entry->alias().c_str());
+ continue;
+ }
+
+ if (blob && blob.isEncrypted()) {
+ *it++ = entry->uid();
+ } else if (charBlob) {
+ auto [success, hwEnforced, swEnforced] = charBlob.getKeyCharacteristics();
+ if (!success) {
+ ALOGE("Error reading blob characteristics for key %s", entry->alias().c_str());
+ continue;
+ }
+ if (hwEnforced.Contains(TAG_USER_SECURE_ID) ||
+ swEnforced.Contains(TAG_USER_SECURE_ID)) {
+ *it++ = entry->uid();
+ }
+ }
+ }
+ *aidl_return = static_cast<int32_t>(ResponseCode::NO_ERROR);
+ return Status::ok();
+}
+
Status KeyStoreService::reset(int32_t* aidl_return) {
if (!checkBinderPermission(P_RESET)) {
*aidl_return = static_cast<int32_t>(ResponseCode::PERMISSION_DENIED);
@@ -507,11 +580,10 @@
return Status::ok();
}
-Status KeyStoreService::clear_uid(int64_t targetUid64, int32_t* aidl_return) {
+Status KeyStoreService::clear_uid(int64_t targetUid64, int32_t* _aidl_return) {
uid_t targetUid = getEffectiveUid(targetUid64);
if (!checkBinderPermissionSelfOrSystem(P_CLEAR_UID, targetUid)) {
- *aidl_return = static_cast<int32_t>(ResponseCode::PERMISSION_DENIED);
- return Status::ok();
+ return AIDL_RETURN(ResponseCode::PERMISSION_DENIED);
}
ALOGI("clear_uid %" PRId64, targetUid64);
@@ -519,16 +591,15 @@
ResponseCode rc;
std::list<LockedKeyBlobEntry> entries;
+ auto userDirName = mKeyStore->getUserStateDB().getUserStateByUid(targetUid)->getUserDirName();
// list has a fence making sure no workers are modifying blob files before iterating the
// data base. All returned entries are locked.
std::tie(rc, entries) = LockedKeyBlobEntry::list(
- mKeyStore->getUserStateDB().getUserStateByUid(targetUid)->getUserDirName(),
- [&](uid_t uid, const std::string&) -> bool { return uid == targetUid; });
+ userDirName, [&](uid_t uid, const std::string&) -> bool { return uid == targetUid; });
if (rc != ResponseCode::NO_ERROR) {
- *aidl_return = static_cast<int32_t>(rc);
- return Status::ok();
+ return AIDL_RETURN(rc);
}
for (LockedKeyBlobEntry& lockedEntry : entries) {
@@ -543,8 +614,7 @@
}
mKeyStore->del(lockedEntry);
}
- *aidl_return = static_cast<int32_t>(ResponseCode::NO_ERROR);
- return Status::ok();
+ return AIDL_RETURN(ResponseCode::NO_ERROR);
}
Status KeyStoreService::addRngEntropy(
@@ -746,7 +816,7 @@
std::tie(rc, keyBlob, charBlob, lockedEntry) =
mKeyStore->getKeyForName(name8, targetUid, TYPE_KEYMASTER_10);
- if (!rc) {
+ if (!rc.isOk()) {
return AIDL_RETURN(rc);
}
diff --git a/keystore/key_store_service.h b/keystore/key_store_service.h
index 601ed21..5a3586f 100644
--- a/keystore/key_store_service.h
+++ b/keystore/key_store_service.h
@@ -61,6 +61,9 @@
int32_t* _aidl_return) override;
::android::binder::Status list(const ::android::String16& namePrefix, int32_t uid,
::std::vector<::android::String16>* _aidl_return) override;
+ ::android::binder::Status listUidsOfAuthBoundKeys(::std::vector<int32_t>* uids,
+ int32_t* _aidl_return) override;
+
::android::binder::Status reset(int32_t* _aidl_return) override;
::android::binder::Status onUserPasswordChanged(int32_t userId,
const ::android::String16& newPassword,
diff --git a/keystore/keymaster_worker.cpp b/keystore/keymaster_worker.cpp
index f3bf71f..6dc055f 100644
--- a/keystore/keymaster_worker.cpp
+++ b/keystore/keymaster_worker.cpp
@@ -88,6 +88,8 @@
std::tie(rc, blob, charBlob) =
lockedEntry.readBlobs(userState->getEncryptionKey(), userState->getState());
+ userState = {};
+
if (rc != ResponseCode::NO_ERROR) {
return error = rc, result;
}
diff --git a/keystore/keystore_cli_v2.cpp b/keystore/keystore_cli_v2.cpp
index 1c94318..777db33 100644
--- a/keystore/keystore_cli_v2.cpp
+++ b/keystore/keystore_cli_v2.cpp
@@ -68,6 +68,7 @@
" delete-all\n"
" exists --name=<key_name>\n"
" list [--prefix=<key_name_prefix>]\n"
+ " list-apps-with-keys\n"
" sign-verify --name=<key_name>\n"
" [en|de]crypt --name=<key_name> --in=<file> --out=<file>\n"
" [--seclevel=software|strongbox|tee(default)]\n"
@@ -287,7 +288,8 @@
return result;
}
-int GenerateKey(const std::string& name, int32_t flags) {
+// Note: auth_bound keys created with this tool will not be usable.
+int GenerateKey(const std::string& name, int32_t flags, bool auth_bound) {
std::unique_ptr<KeystoreClient> keystore = CreateKeystoreInstance();
AuthorizationSetBuilder params;
params.RsaSigningKey(2048, 65537)
@@ -296,8 +298,14 @@
.Digest(Digest::SHA_2_384)
.Digest(Digest::SHA_2_512)
.Padding(PaddingMode::RSA_PKCS1_1_5_SIGN)
- .Padding(PaddingMode::RSA_PSS)
- .Authorization(TAG_NO_AUTH_REQUIRED);
+ .Padding(PaddingMode::RSA_PSS);
+ if (auth_bound) {
+ // Gatekeeper normally generates the secure user id.
+ // Using zero allows the key to be created, but it will not be usuable.
+ params.Authorization(TAG_USER_SECURE_ID, 0);
+ } else {
+ params.Authorization(TAG_NO_AUTH_REQUIRED);
+ }
AuthorizationSet hardware_enforced_characteristics;
AuthorizationSet software_enforced_characteristics;
auto result = keystore->generateKey(name, params, flags, &hardware_enforced_characteristics,
@@ -366,6 +374,35 @@
return 0;
}
+int ListAppsWithKeys() {
+
+ sp<android::IServiceManager> sm = android::defaultServiceManager();
+ sp<android::IBinder> binder = sm->getService(String16("android.security.keystore"));
+ sp<IKeystoreService> service = android::interface_cast<IKeystoreService>(binder);
+ if (service == nullptr) {
+ fprintf(stderr, "Error connecting to keystore service.\n");
+ return 1;
+ }
+ int32_t aidl_return;
+ ::std::vector<int32_t> uids(100);
+ android::binder::Status status = service->listUidsOfAuthBoundKeys(&uids, &aidl_return);
+ if (!status.isOk()) {
+ fprintf(stderr, "Requesting uids of auth bound keys failed with error %s.\n",
+ status.toString8().c_str());
+ return 1;
+ }
+ if (!KeyStoreNativeReturnCode(aidl_return).isOk()) {
+ fprintf(stderr, "Requesting uids of auth bound keys failed with code %d.\n", aidl_return);
+ return 1;
+ }
+ printf("Apps with auth bound keys:\n");
+ for (auto i = uids.begin(); i != uids.end(); ++i) {
+ if (*i == 0) break;
+ printf("%d\n", *i);
+ }
+ return 0;
+}
+
int SignAndVerify(const std::string& name) {
std::unique_ptr<KeystoreClient> keystore = CreateKeystoreInstance();
AuthorizationSetBuilder sign_params;
@@ -593,8 +630,7 @@
CommandLine* command_line = CommandLine::ForCurrentProcess();
CommandLine::StringVector args = command_line->GetArgs();
- std::thread thread_pool([] { android::IPCThreadState::self()->joinThreadPool(false); });
- thread_pool.detach();
+ android::ProcessState::self()->startThreadPool();
if (args.empty()) {
PrintUsageAndExit();
@@ -609,7 +645,8 @@
securityLevelOption2Flags(*command_line));
} else if (args[0] == "generate") {
return GenerateKey(command_line->GetSwitchValueASCII("name"),
- securityLevelOption2Flags(*command_line));
+ securityLevelOption2Flags(*command_line),
+ command_line->HasSwitch("auth_bound"));
} else if (args[0] == "get-chars") {
return GetCharacteristics(command_line->GetSwitchValueASCII("name"));
} else if (args[0] == "export") {
@@ -622,6 +659,8 @@
return DoesKeyExist(command_line->GetSwitchValueASCII("name"));
} else if (args[0] == "list") {
return List(command_line->GetSwitchValueASCII("prefix"));
+ } else if (args[0] == "list-apps-with-keys") {
+ return ListAppsWithKeys();
} else if (args[0] == "sign-verify") {
return SignAndVerify(command_line->GetSwitchValueASCII("name"));
} else if (args[0] == "encrypt") {
diff --git a/keystore/tests/list_auth_bound_keys_test.sh b/keystore/tests/list_auth_bound_keys_test.sh
new file mode 100755
index 0000000..f609b34
--- /dev/null
+++ b/keystore/tests/list_auth_bound_keys_test.sh
@@ -0,0 +1,77 @@
+#!/usr/bin/env bash
+
+#
+# Copyright (C) 2018 The Android Open Source Project
+#
+# 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.
+#
+#
+# Simple adb based test for keystore method list_auth_bound_keys
+# Depends on keystore_cli_v2 tool and root
+#
+
+set -e
+
+ROOT_ID=0
+USER1_ID=10901
+USER2_ID=10902
+SYSTEM_ID=1000
+
+function cli {
+ adb shell "su $1 keystore_cli_v2 $2"
+}
+
+#start as root
+adb root
+
+# generate keys as user
+echo "generating keys"
+cli $USER1_ID "delete --name=no_auth_key" || true
+cli $USER1_ID "generate --name=no_auth_key"
+cli $USER2_ID "delete --name=auth_key" || true
+if ! cli $USER2_ID "generate --name=auth_key --auth_bound"; then
+ echo "Unable to generate auth bound key, make sure device/emulator has a pin/password set."
+ echo "$ adb shell locksettings set-pin 1234"
+ exit 1
+fi
+
+# try to list keys as user
+if cli $USER2_ID list-apps-with-keys; then
+ echo "Error: list-apps-with-keys succeeded as user, this is not expected!"
+ exit 1
+fi
+
+# try to list keys as root
+if cli $ROOT_ID "list-apps-with-keys"; then
+ echo "Error: list-apps-with-keys succeeded as root, this is not expected!"
+ exit 1
+fi
+
+# try to list keys as system
+success=false
+while read -r line; do
+ echo $line
+ if [ "$line" == "$USER2_ID" ]; then
+ success=true
+ fi
+ if [ "$line" == "$USER1_ID" ]; then
+ echo "Error: User1 id not expected in list"
+ exit 1
+ fi
+done <<< $(cli $SYSTEM_ID "list-apps-with-keys")
+if [ $success = true ]; then
+ echo "Success!"
+else
+ echo "Error: User2 id not in list"
+ exit 1
+fi
\ No newline at end of file