Merge "Fix long-edge vs short-edge duplex in PWG-Raster"
diff --git a/jni/include/lib_wprint.h b/jni/include/lib_wprint.h
index 006f6c1..147e557 100644
--- a/jni/include/lib_wprint.h
+++ b/jni/include/lib_wprint.h
@@ -188,6 +188,10 @@
     char docCategory[10];
     const char *media_default;
 
+    // Expected certificate if any
+    uint8 *certificate;
+    int certificate_len;
+
     // IPP max job-name is 2**31 - 1, we set a shorter limit
     char job_name[MAX_ID_STRING_LENGTH + 1];
     char job_originating_user_name[MAX_NAME_LENGTH + 1];
@@ -198,17 +202,23 @@
     bool accepts_os_version;
 } wprint_job_params_t;
 
+typedef struct wprint_connect_info_st wprint_connect_info_t;
+
 /*
  * Parameters defining how to reach a remote printing service
  */
-typedef struct {
+struct wprint_connect_info_st {
     const char *printer_addr;
     const char *uri_path;
     const char *uri_scheme;
     int port_num;
     /* Timeout per retry in milliseconds */
     long timeout;
-} wprint_connect_info_t;
+    /* Return non-0 if the received certificate is not acceptable. */
+    int (*validate_certificate)(struct wprint_connect_info_st *connect_info, uint8 *data, int data_len);
+    /* User-supplied data. */
+    void *user;
+};
 
 /*
  * Current state of a queued job
@@ -224,6 +234,9 @@
     wprint_job_state_t state;
     unsigned int blocked_reasons;
     int job_done_result;
+    // Certificate received from printer, if any
+    uint8 *certificate;
+    int certificate_len;
 } wprint_job_callback_params_t;
 
 typedef enum {
diff --git a/jni/include/wprint_status_types.h b/jni/include/wprint_status_types.h
index 9fe1b1d..80b3c74 100644
--- a/jni/include/wprint_status_types.h
+++ b/jni/include/wprint_status_types.h
@@ -38,7 +38,8 @@
 #define BLOCKED_REASON_IDLE              (1 << PRINT_STATUS_IDLE)
 #define BLOCKED_REASON_CANCELLED         (1 << PRINT_STATUS_CANCELLED)
 #define BLOCKED_REASON_PRINT_STATUS_VERY_LOW_ON_INK (1 << PRINT_STATUS_VERY_LOW_ON_INK)
-#define BLOCKED_REASON_PARTIAL_CANCEL   (1 << PRINT_STATUS_PARTIAL_CANCEL)
+#define BLOCKED_REASON_PARTIAL_CANCEL    (1 << PRINT_STATUS_PARTIAL_CANCEL)
+#define BLOCKED_REASON_BAD_CERTIFICATE   (1 << PRINT_STATUS_BAD_CERTIFICATE)
 
 /*
  * Enumeration for printer statuses
@@ -68,6 +69,7 @@
 
     PRINT_STATUS_VERY_LOW_ON_INK,
     PRINT_STATUS_PARTIAL_CANCEL,
+    PRINT_STATUS_BAD_CERTIFICATE,
 
     PRINT_STATUS_MAX_STATE // Add new entries above this line.
 } print_status_t;
diff --git a/jni/ipphelper/ipphelper.c b/jni/ipphelper/ipphelper.c
index 60758ac..ec01564 100644
--- a/jni/ipphelper/ipphelper.c
+++ b/jni/ipphelper/ipphelper.c
@@ -643,8 +643,6 @@
         LOGD("media-supported  found; number of values %d", ippGetCount(attrptr));
         for (i = 0; i < ippGetCount(attrptr); i++) {
             idx = ipp_find_media_size(ippGetString(attrptr, i, NULL), &media_sizeTemp);
-            LOGD(" Temp - i: %d  idx %d keyword: %s PT_size %d", i, idx, ippGetString(
-                    attrptr, i, NULL), media_sizeTemp);
 
             // Modified since anytime the find media size returned 0 it could either mean
             // NOT found or na_letter.
@@ -1171,11 +1169,29 @@
     }
 }
 
+/*
+ * Handle server certificate information.
+ */
+static int ipp_server_cert_cb(http_t *http, void *tls, cups_array_t *certs, void *user_data) {
+    wprint_connect_info_t *connect_info = (wprint_connect_info_t *)user_data;
+    int error = 0;
+    if (connect_info->validate_certificate) {
+        http_credential_t *credential = cupsArrayFirst(certs);
+        if (credential) {
+            LOGD("ipp_server_cert_cb: validate_certificate (len=%d)", credential->datalen);
+            error = connect_info->validate_certificate(connect_info, credential->data, credential->datalen);
+        }
+    }
+    return error;
+}
+
 http_t *ipp_cups_connect(const wprint_connect_info_t *connect_info, char *printer_uri,
         unsigned int uriLength) {
     const char *uri_path;
     http_t *curl_http = NULL;
 
+    cupsSetServerCertCB(ipp_server_cert_cb, (void *)connect_info);
+
     if ((connect_info->uri_path == NULL) || (strlen(connect_info->uri_path) == 0)) {
         uri_path = DEFAULT_IPP_URI_RESOURCE;
     } else {
@@ -1202,6 +1218,8 @@
     if (curl_http == NULL) {
         LOGD("ipp_cups_connect failed addr=%s port=%d", connect_info->printer_addr, ippPortNumber);
     }
+
+    cupsSetServerCertCB(NULL, NULL);
     return curl_http;
 }
 
diff --git a/jni/lib/lib_wprint.c b/jni/lib/lib_wprint.c
index e7aebfe..8af47d6 100644
--- a/jni/lib/lib_wprint.c
+++ b/jni/lib/lib_wprint.c
@@ -154,6 +154,10 @@
     char printer_uri[1024];
     int job_debug_fd;
     int page_debug_fd;
+
+    /* A buffer of bytes containing the certificate received while setting up this job, if any. */
+    uint8 *certificate;
+    int certificate_len;
 } _job_queue_t;
 
 /*
@@ -480,7 +484,10 @@
         }
         jq->page_debug_fd = -1;
         jq->debug_path[0] = 0;
-
+        if (jq->certificate) {
+            free(jq->certificate);
+            jq->certificate = NULL;
+        }
         return OK;
     } else {
         return ERROR;
@@ -516,6 +523,8 @@
 
     statusnew = new_status->printer_status & ~PRINTER_IDLE_BIT;
     statusold = old_status->printer_status & ~PRINTER_IDLE_BIT;
+    cb_param.certificate = jq->certificate;
+    cb_param.certificate_len = jq->certificate_len;
 
     LOGD("_job_status_callback(): current printer state: %d", statusnew);
     blocked_reasons = 0;
@@ -689,10 +698,86 @@
 }
 
 /*
+ * Return true unless the server gave an unexpected certificate
+ */
+static bool _is_certificate_allowed(_job_queue_t *jq) {
+    int result = true;
+
+    // Compare certificates if both are known
+    if (jq->job_params.certificate && jq->certificate) {
+        if (jq->job_params.certificate_len != jq->certificate_len) {
+            LOGD("_is_certificate_allowed: certificate length mismatch allowed=%d, received=%d",
+                jq->job_params.certificate_len, jq->certificate_len);
+            result = false;
+        } else if (0 != memcmp(jq->job_params.certificate, jq->certificate, jq->certificate_len)) {
+            LOGD("_is_certificate_allowed: certificate content mismatch");
+            result = false;
+        } else {
+            LOGD("_is_certificate_allowed: certificate match, len=%d",
+                jq->job_params.certificate_len);
+        }
+    }
+
+    return result;
+}
+
+/*
+ * Callback from lower layers containing certificate data, if any.
+ */
+static int _validate_certificate(wprint_connect_info_t *connect_info, uint8 *data, int data_len) {
+    _job_queue_t *jq = connect_info->user;
+    LOGD("_validate_certificate: %s://%s:%d%s handling server cert len=%d for job %ld",
+        connect_info->uri_scheme, connect_info->printer_addr, connect_info->port_num,
+        connect_info->uri_path, data_len, jq->job_handle);
+
+    // Free any old certificate we have and save new certificate data
+    if (jq->certificate) {
+        free(jq->certificate);
+        jq->certificate = NULL;
+    }
+    jq->certificate = (uint8 *)malloc(data_len);
+    int error = 0;
+    if (jq->certificate == NULL) {
+        LOGD("_validate_certificate: malloc failed");
+        error = -1;
+    } else {
+        memcpy(jq->certificate, data, data_len);
+        jq->certificate_len = data_len;
+        if (!_is_certificate_allowed(jq)) {
+            LOGD("_validate_certificate: received certificate disallowed.");
+            error = -1;
+        }
+    }
+    return error;
+}
+
+/*
+ * Initialize the status interface (so we can use it to query for printer status.
+ */
+static void _initialize_status_ifc(_job_queue_t *jq) {
+    wprint_connect_info_t connect_info;
+    connect_info.printer_addr = jq->printer_addr;
+    connect_info.uri_path = jq->printer_uri;
+    connect_info.port_num = jq->port_num;
+    if (jq->use_secure_uri) {
+        connect_info.uri_scheme = IPPS_PREFIX;
+        connect_info.user = jq;
+        connect_info.validate_certificate = _validate_certificate;
+    } else {
+        connect_info.uri_scheme = IPP_PREFIX;
+        connect_info.validate_certificate = NULL;
+    }
+    connect_info.timeout = DEFAULT_IPP_TIMEOUT;
+
+    // Initialize the status interface with this connection info
+    jq->status_ifc->init(jq->status_ifc, &connect_info);
+}
+
+/*
  * Runs a print job. Contains logic for what to do given different printer statuses.
  */
 static void *_job_thread(void *param) {
-    wprint_job_callback_params_t cb_param;
+    wprint_job_callback_params_t cb_param = { 0 };
     _msg_t msg;
     wJob_t job_handle;
     _job_queue_t *jq;
@@ -737,17 +822,7 @@
 
             // initialize the status ifc
             if (jq->status_ifc != NULL) {
-                wprint_connect_info_t connect_info;
-                connect_info.printer_addr = jq->printer_addr;
-                connect_info.uri_path = jq->printer_uri;
-                connect_info.port_num = jq->port_num;
-                if (jq->use_secure_uri) {
-                    connect_info.uri_scheme = IPPS_PREFIX;
-                } else {
-                    connect_info.uri_scheme = IPP_PREFIX;
-                }
-                connect_info.timeout = DEFAULT_IPP_TIMEOUT;
-                jq->status_ifc->init(jq->status_ifc, &connect_info);
+                _initialize_status_ifc(jq);
             }
             // wait for the printer to be idle
             if ((jq->status_ifc != NULL) && (jq->status_ifc->get_status != NULL)) {
@@ -759,6 +834,10 @@
                     jq->status_ifc->get_status(jq->status_ifc, &printer_state);
                     status = printer_state.printer_status & ~PRINTER_IDLE_BIT;
 
+                    // Pass along any certificate received in future callbacks
+                    cb_param.certificate = jq->certificate;
+                    cb_param.certificate_len = jq->certificate_len;
+
                     switch (status) {
                         case PRINT_STATUS_IDLE:
                             printer_state.printer_status = PRINT_STATUS_IDLE;
@@ -776,8 +855,13 @@
                         case PRINT_STATUS_SVC_REQUEST:
                             if ((printer_state.printer_reasons[0] == PRINT_STATUS_UNABLE_TO_CONNECT)
                                     || (printer_state.printer_reasons[0] == PRINT_STATUS_OFFLINE)) {
-                                LOGD("_job_thread: Received an Unable to Connect message");
-                                jq->blocked_reasons = BLOCKED_REASON_UNABLE_TO_CONNECT;
+                                if (_is_certificate_allowed(jq)) {
+                                    LOGD("_job_thread: Received an Unable to Connect message");
+                                    jq->blocked_reasons = BLOCKED_REASON_UNABLE_TO_CONNECT;
+                                } else {
+                                    LOGD("_job_thread: Bad certificate");
+                                    jq->blocked_reasons = BLOCKED_REASON_BAD_CERTIFICATE;
+                                }
                                 loop = 0;
                                 break;
                             }
@@ -2047,6 +2131,8 @@
                 cb_param.state = JOB_DONE;
                 cb_param.blocked_reasons = BLOCKED_REASONS_CANCELLED;
                 cb_param.job_done_result = CANCELLED;
+                cb_param.certificate = jq->certificate;
+                cb_param.certificate_len = jq->certificate_len;
 
                 jq->cb_fn(job_handle, (void *) &cb_param);
             }
diff --git a/jni/lib/wprintJNI.c b/jni/lib/wprintJNI.c
index 913198a..42c27ca 100644
--- a/jni/lib/wprintJNI.c
+++ b/jni/lib/wprintJNI.c
@@ -75,6 +75,7 @@
 static jfieldID _LocalPrinterCapabilitiesField__supportedMediaTypes;
 static jfieldID _LocalPrinterCapabilitiesField__supportedMediaSizes;
 static jfieldID _LocalPrinterCapabilitiesField__nativeData;
+static jfieldID _LocalPrinterCapabilitiesField__certificate;
 
 static jclass _JobCallbackClass;
 static jobject _callbackReceiver;
@@ -86,6 +87,7 @@
 static jfieldID _JobCallbackParamsField__jobState;
 static jfieldID _JobCallbackParamsField__jobDoneResult;
 static jfieldID _JobCallbackParamsField__blockedReasons;
+static jfieldID _JobCallbackParamsField__certificate;
 
 static jclass _PrintServiceStringsClass;
 static jfieldID _PrintServiceStringsField__JOB_STATE_QUEUED;
@@ -110,6 +112,7 @@
 static jfieldID _PrintServiceStringsField__BLOCKED_REASON__LOW_ON_INK;
 static jfieldID _PrintServiceStringsField__BLOCKED_REASON__LOW_ON_TONER;
 static jfieldID _PrintServiceStringsField__BLOCKED_REASON__REALLY_LOW_ON_INK;
+static jfieldID _PrintServiceStringsField__BLOCKED_REASON__BAD_CERTIFICATE;
 static jfieldID _PrintServiceStringsField__BLOCKED_REASON__UNKNOWN;
 static jfieldID _PrintServiceStringsField__ALIGNMENT__CENTER;
 static jfieldID _PrintServiceStringsField__ALIGNMENT__CENTER_HORIZONTAL;
@@ -482,6 +485,8 @@
             env, _LocalPrinterCapabilitiesClass, "supportedMediaSizes", "[I");
     _LocalPrinterCapabilitiesField__nativeData = (*env)->GetFieldID(
             env, _LocalPrinterCapabilitiesClass, "nativeData", "[B");
+    _LocalPrinterCapabilitiesField__certificate = (*env)->GetFieldID(
+            env, _LocalPrinterCapabilitiesClass, "certificate", "[B");
 
     _JobCallbackParamsClass = (jclass) (*env)->NewGlobalRef(env, (*env)->FindClass(
             env, "com/android/bips/jni/JobCallbackParams"));
@@ -495,6 +500,8 @@
             env, _JobCallbackParamsClass, "jobDoneResult", "Ljava/lang/String;");
     _JobCallbackParamsField__blockedReasons = (*env)->GetFieldID(
             env, _JobCallbackParamsClass, "blockedReasons", "[Ljava/lang/String;");
+    _JobCallbackParamsField__certificate = (*env)->GetFieldID(
+            env, _JobCallbackParamsClass, "certificate", "[B");
 
     if (callbackReceiver) {
         _callbackReceiver = (jobject) (*env)->NewGlobalRef(env, callbackReceiver);
@@ -557,6 +564,9 @@
     _PrintServiceStringsField__BLOCKED_REASON__REALLY_LOW_ON_INK = (*env)->GetStaticFieldID(
             env, _PrintServiceStringsClass, "BLOCKED_REASON__REALLY_LOW_ON_INK",
             "Ljava/lang/String;");
+    _PrintServiceStringsField__BLOCKED_REASON__BAD_CERTIFICATE = (*env)->GetStaticFieldID(
+            env, _PrintServiceStringsClass, "BLOCKED_REASON__BAD_CERTIFICATE",
+            "Ljava/lang/String;");
     _PrintServiceStringsField__BLOCKED_REASON__UNKNOWN = (*env)->GetStaticFieldID(
             env, _PrintServiceStringsClass, "BLOCKED_REASON__UNKNOWN", "Ljava/lang/String;");
 
@@ -589,7 +599,7 @@
     }
     jbyte *nativeDataPtr = (*env)->GetByteArrayElements(env, nativeDataObject, NULL);
     memcpy(wprintPrinterCaps, (const void *) nativeDataPtr, sizeof(printer_capabilities_t));
-    (*env)->ReleaseByteArrayElements(env, nativeDataObject, nativeDataPtr, JNI_ABORT);
+    (*env)->ReleaseByteArrayElements(env, nativeDataObject, nativeDataPtr, 0);
 
     return OK;
 }
@@ -1076,6 +1086,10 @@
                     jStr = (jstring) (*env)->GetStaticObjectField(
                             env, _PrintServiceStringsClass,
                             _PrintServiceStringsField__BLOCKED_REASON__REALLY_LOW_ON_INK);
+                } else if (blocked_reasons & BLOCKED_REASON_BAD_CERTIFICATE) {
+                    jStr = (jstring) (*env)->GetStaticObjectField(
+                            env, _PrintServiceStringsClass,
+                            _PrintServiceStringsField__BLOCKED_REASON__BAD_CERTIFICATE);
                 } else if (blocked_reasons & BLOCKED_REASON_UNKNOWN) {
                     jStr = (jstring) (*env)->GetStaticObjectField(
                             env, _PrintServiceStringsClass,
@@ -1094,6 +1108,24 @@
 
         (*env)->SetIntField(env, callbackParams, _JobCallbackParamsField__jobId,
                 (jint) job_handle);
+
+        if (cb_param->certificate) {
+            LOGI("_wprint_callback_fn: copying certificate len=%d", cb_param->certificate_len);
+            jbyteArray certificate = (*env)->NewByteArray(env, cb_param->certificate_len);
+            jbyte *certificateBytes = (*env)->GetByteArrayElements(env, certificate, 0);
+            memcpy(certificateBytes, (const void *) cb_param->certificate,
+                cb_param->certificate_len);
+            (*env)->ReleaseByteArrayElements(env, certificate, certificateBytes, 0);
+            (*env)->SetObjectField(env, callbackParams, _JobCallbackParamsField__certificate,
+                certificate);
+            (*env)->DeleteLocalRef(env, certificate);
+        } else {
+            LOGI("_wprint_callback_fn: there is no certificate");
+            // No cert, set NULL
+            (*env)->SetObjectField(env, callbackParams, _JobCallbackParamsField__certificate,
+                NULL);
+        }
+
         (*env)->CallVoidMethod(env, _callbackReceiver, _JobCallbackMethod__jobCallback,
                 (jint) job_handle, callbackParams);
         (*env)->DeleteLocalRef(env, callbackParams);
@@ -1160,6 +1192,7 @@
     connect_info.uri_scheme = copyToNewString(env, uriScheme);
     connect_info.port_num = port;
     connect_info.timeout = timeout;
+    connect_info.validate_certificate = NULL;
 
     LOGI("nativeGetCapabilities for %s JNIenv is %p", connect_info.printer_addr, env);
 
@@ -1222,6 +1255,24 @@
 }
 
 /*
+ * Convert certificate (if present) from printer capabilities into job_params.
+ */
+static void _convertCertificate(JNIEnv *env, jobject printerCaps, wprint_job_params_t *params) {
+    params->certificate = NULL;
+    jbyteArray certificate = (jbyteArray) (*env)->GetObjectField(env, printerCaps,
+            _LocalPrinterCapabilitiesField__certificate);
+    if (certificate) {
+        params->certificate_len = (*env)->GetArrayLength(env, certificate);
+        params->certificate = malloc(params->certificate_len);
+        if (params->certificate) {
+            jbyte *certificateBytes = (*env)->GetByteArrayElements(env, certificate, NULL);
+            memcpy(params->certificate, certificateBytes, params->certificate_len);
+            (*env)->ReleaseByteArrayElements(env, certificate, certificateBytes, JNI_ABORT);
+        }
+    }
+}
+
+/*
  * JNI call to wprint to start a print job. Takes connection params, job params, caps, and file
  * array to complete the job
  */
@@ -1238,6 +1289,7 @@
 
     _convertJobParams_to_C(env, jobParams, &params);
     _convertPrinterCaps_to_C(env, printerCaps, &caps);
+    _convertCertificate(env, printerCaps, &params);
 
     LOGD("nativeStartJob: After _convertJobParams_to_C: res=%d, name=%s",
             params.pdf_render_resolution, params.job_name);
@@ -1360,6 +1412,9 @@
         }
     }
 
+    if (params.certificate) {
+        free(params.certificate);
+    }
     (*env)->ReleaseStringUTFChars(env, mimeType, mimeTypeStr);
     (*env)->ReleaseStringUTFChars(env, address, addressStr);
     (*env)->ReleaseStringUTFChars(env, _fakeDir, dataDirStr);
diff --git a/res/drawable/ic_printer_locked.xml b/res/drawable/ic_printer_locked.xml
new file mode 100644
index 0000000..fbb57bd
--- /dev/null
+++ b/res/drawable/ic_printer_locked.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     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.
+-->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="36dp"
+    android:height="36dp"
+    android:viewportWidth="48.0"
+    android:viewportHeight="48.0"
+    android:tint="@color/iconColor">
+    <path
+        android:pathData="M 16.96 6 L 16.96 14 L 36 14 L 36 6 L 16.96 6 Z M 9.408 6.028 C 7.427 6.028 5.819 7.635 5.819 9.616 L 5.819 11.769 C 5.03 11.769 4.384 12.414 4.384 13.204 L 4.384 18.945 C 4.384 19.734 5.03 20.38 5.819 20.38 L 12.996 20.38 C 13.785 20.38 14.431 19.734 14.431 18.945 L 14.431 13.204 C 14.431 12.414 13.785 11.769 12.996 11.769 L 12.996 9.616 C 12.996 7.635 11.388 6.028 9.408 6.028 Z M 9.408 7.463 C 10.599 7.463 11.561 8.425 11.561 9.616 L 11.561 11.769 L 7.254 11.769 L 7.254 9.616 C 7.254 8.425 8.216 7.463 9.408 7.463 Z M 16.96 16 L 16.96 19.155 C 16.976 20.614 16.078 22.989 13.362 22.973 L 4 22.984 L 4 34 L 12 34 L 12 42 L 36 42 L 36 34 L 44 34 L 44 22 C 44 18.69 41.31 16 38 16 L 16.96 16 Z M 38 20 C 39.11 20 40 20.89 40 22 C 40 23.11 39.11 24 38 24 C 36.89 24 36 23.11 36 22 C 36 20.89 36.89 20 38 20 Z M 16 28 L 32 28 L 32 38 L 16 38 L 16 28 Z"
+        android:fillColor="@android:color/black" />
+</vector>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index eac63ce..015e6b4 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -29,6 +29,7 @@
     <string name="printer_door_open">Door open</string>
     <string name="printer_jammed">Jammed</string>
     <string name="printer_offline">Offline</string>
+    <string name="printer_bad_certificate">Bad certificate</string>
     <string name="printer_check">Check printer</string>
     <string name="waiting_to_send">Waiting to send</string>
 
@@ -81,5 +82,14 @@
     <string name="connects_via_network">Connects via current network at
         <xliff:g example="192.168.0.101" id="ip_address">%1$s</xliff:g>
     </string>
+    <!-- Channel name for security-related notifications [CHAR LIMIT=40] -->
+    <string name="security">Security</string>
+    <!-- Message shown in notification if a printer presented a changes security certificate [CHAR LIMIT=UNLIMITED] -->
+    <string name="certificate_update_request">This printer provided a new security certificate, or another
+        device is impersonating it. Accept the new certificate?</string>
+    <!-- Button label in a notification. This button a accepts a changed security certificate presented by a printer [CHAR LIMIT=20] -->
+    <string name="accept">Accept</string>
+    <!-- Button label in a notification. This button a rejects a changed security certificate presented by a printer [CHAR LIMIT=20] -->
+    <string name="reject">Reject</string>
 
 </resources>
diff --git a/src/com/android/bips/BuiltInPrintService.java b/src/com/android/bips/BuiltInPrintService.java
index 2bb73ef..9e5d156 100644
--- a/src/com/android/bips/BuiltInPrintService.java
+++ b/src/com/android/bips/BuiltInPrintService.java
@@ -17,13 +17,20 @@
 
 package com.android.bips;
 
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
 import android.content.BroadcastReceiver;
 import android.content.Context;
+import android.content.Intent;
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
+import android.graphics.drawable.Icon;
 import android.net.nsd.NsdManager;
 import android.net.wifi.WifiManager;
 import android.os.Handler;
+import android.print.PrinterId;
 import android.printservice.PrintJob;
 import android.printservice.PrintService;
 import android.printservice.PrinterDiscoverySession;
@@ -40,6 +47,7 @@
 import com.android.bips.discovery.P2pDiscovery;
 import com.android.bips.ipp.Backend;
 import com.android.bips.ipp.CapabilitiesCache;
+import com.android.bips.ipp.CertificateStore;
 import com.android.bips.p2p.P2pMonitor;
 import com.android.bips.p2p.P2pUtils;
 import com.android.bips.util.BroadcastMonitor;
@@ -51,6 +59,17 @@
     private static final boolean DEBUG = false;
     private static final int IPPS_PRINTER_DELAY = 150;
     private static final int P2P_DISCOVERY_DELAY = 1000;
+    private static final String CHANNEL_ID_SECURITY = "security";
+    private static final String TAG_CERTIFICATE_REQUEST =
+            BuiltInPrintService.class.getCanonicalName() + ".CERTIFICATE_REQUEST";
+    private static final String ACTION_CERTIFICATE_ACCEPT =
+            BuiltInPrintService.class.getCanonicalName() + ".CERTIFICATE_ACCEPT";
+    private static final String ACTION_CERTIFICATE_REJECT =
+            BuiltInPrintService.class.getCanonicalName() + ".CERTIFICATE_REJECT";
+    private static final String EXTRA_CERTIFICATE = "certificate";
+    private static final String EXTRA_PRINTER_ID = "printer-id";
+    private static final String EXTRA_PRINTER_UUID = "printer-uuid";
+    private static final int CERTIFICATE_REQUEST_ID = 1000;
 
     // Present because local activities can bind, but cannot access this object directly
     private static WeakReference<BuiltInPrintService> sInstance;
@@ -60,6 +79,7 @@
     private Discovery mMdnsDiscovery;
     private ManualDiscovery mManualDiscovery;
     private CapabilitiesCache mCapabilitiesCache;
+    private CertificateStore mCertificateStore;
     private JobQueue mJobQueue;
     private Handler mMainHandler;
     private Backend mBackend;
@@ -85,9 +105,11 @@
             }
         }
         super.onCreate();
+        createNotificationChannel();
 
         sInstance = new WeakReference<>(this);
         mBackend = new Backend(this);
+        mCertificateStore = new CertificateStore(this);
         mCapabilitiesCache = new CapabilitiesCache(this, mBackend,
                 CapabilitiesCache.DEFAULT_MAX_CONCURRENT);
         mP2pMonitor = new P2pMonitor(this);
@@ -200,6 +222,13 @@
     }
 
     /**
+     * Return a store of certificate public keys for supporting trust-on-first-use.
+     */
+    public CertificateStore getCertificateStore() {
+        return mCertificateStore;
+    }
+
+    /**
      * Return the main handler for posting {@link Runnable} objects to the main UI
      */
     public Handler getMainHandler() {
@@ -228,6 +257,13 @@
         }
     }
 
+    /**
+     * Return an icon ID appropriate for displaying a printer.
+     */
+    public int getIconId(DiscoveredPrinter printer) {
+        return printer.isSecure() ? R.drawable.ic_printer_locked : R.drawable.ic_printer;
+    }
+
     /** Prevent Wi-Fi from going to sleep until {@link #unlockWifi} is called */
     public void lockWifi() {
         if (!mWifiLock.isHeld()) {
@@ -241,4 +277,83 @@
             mWifiLock.release();
         }
     }
+
+    /**
+     * Set up a channel for notifications.
+     */
+    private void createNotificationChannel() {
+        NotificationChannel channel = new NotificationChannel(CHANNEL_ID_SECURITY,
+                getString(R.string.security), NotificationManager.IMPORTANCE_HIGH);
+
+        NotificationManager manager = (NotificationManager) getSystemService(
+                Context.NOTIFICATION_SERVICE);
+        manager.createNotificationChannel(channel);
+    }
+
+    /**
+     * Notify the user of a certificate change (could be a MITM attack) and allow response.
+     */
+    void notifyCertificateChange(String printerName, PrinterId printerId, String printerUuid,
+                                 byte[] certificate) {
+        String message = getString(R.string.certificate_update_request);
+
+        Intent rejectIntent = new Intent(this, BuiltInPrintService.class)
+                .setAction(ACTION_CERTIFICATE_REJECT)
+                .putExtra(EXTRA_PRINTER_ID, printerId);
+        PendingIntent pendingRejectIntent = PendingIntent.getService(this, CERTIFICATE_REQUEST_ID,
+                rejectIntent, PendingIntent.FLAG_UPDATE_CURRENT);
+        Notification.Action rejectAction = new Notification.Action.Builder(
+                Icon.createWithResource(this, R.drawable.ic_printservice),
+                getString(R.string.reject), pendingRejectIntent).build();
+
+        PendingIntent deleteIntent = PendingIntent.getService(this, CERTIFICATE_REQUEST_ID,
+                rejectIntent, 0);
+
+        Intent acceptIntent = new Intent(this, BuiltInPrintService.class)
+                .setAction(ACTION_CERTIFICATE_ACCEPT)
+                .putExtra(EXTRA_CERTIFICATE, certificate)
+                .putExtra(EXTRA_PRINTER_UUID, printerUuid)
+                .putExtra(EXTRA_PRINTER_ID, printerId);
+        PendingIntent pendingAcceptIntent = PendingIntent.getService(this, CERTIFICATE_REQUEST_ID,
+                acceptIntent, PendingIntent.FLAG_UPDATE_CURRENT);
+        Notification.Action acceptAction = new Notification.Action.Builder(
+                Icon.createWithResource(this, R.drawable.ic_printservice),
+                getString(R.string.accept), pendingAcceptIntent).build();
+
+        Notification notification = new Notification.Builder(this, CHANNEL_ID_SECURITY)
+                .setContentTitle(printerName)
+                .setSmallIcon(R.drawable.ic_printservice)
+                .setStyle(new Notification.BigTextStyle().bigText(message))
+                .setContentText(message)
+                .setAutoCancel(true)
+                .addAction(rejectAction)
+                .addAction(acceptAction)
+                .setDeleteIntent(deleteIntent)
+                .build();
+
+        NotificationManager manager = (NotificationManager) getSystemService(
+                Context.NOTIFICATION_SERVICE);
+        manager.notify(TAG_CERTIFICATE_REQUEST, CERTIFICATE_REQUEST_ID, notification);
+    }
+
+    @Override
+    public int onStartCommand(Intent intent, int flags, int startId) {
+        if (DEBUG) Log.d(TAG, "Received action=" + intent.getAction());
+        if (ACTION_CERTIFICATE_ACCEPT.equals(intent.getAction())) {
+            byte[] certificate = intent.getByteArrayExtra(EXTRA_CERTIFICATE);
+            PrinterId printerId = intent.getParcelableExtra(EXTRA_PRINTER_ID);
+            String printerUuid = intent.getStringExtra(EXTRA_PRINTER_UUID);
+            mCertificateStore.put(printerUuid, certificate);
+            // Restart the job with the new certificate in place
+            mJobQueue.restart(printerId);
+        } else if (ACTION_CERTIFICATE_REJECT.equals(intent.getAction())) {
+            // Cancel any job in certificate state for this uuid
+            PrinterId printerId = intent.getParcelableExtra(EXTRA_PRINTER_ID);
+            mJobQueue.cancel(printerId);
+        }
+        NotificationManager manager = (NotificationManager) getSystemService(
+                Context.NOTIFICATION_SERVICE);
+        manager.cancel(TAG_CERTIFICATE_REQUEST, CERTIFICATE_REQUEST_ID);
+        return START_NOT_STICKY;
+    }
 }
diff --git a/src/com/android/bips/JobQueue.java b/src/com/android/bips/JobQueue.java
index 6b64b41..f34dcdf 100644
--- a/src/com/android/bips/JobQueue.java
+++ b/src/com/android/bips/JobQueue.java
@@ -18,13 +18,16 @@
 package com.android.bips;
 
 import android.print.PrintJobId;
+import android.print.PrinterId;
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.UUID;
+import java.util.concurrent.CopyOnWriteArrayList;
 
 /** Manages a job queue, ensuring only one job is printed at a time */
 class JobQueue {
-    private final List<LocalPrintJob> mJobs = new ArrayList<>();
+    private final List<LocalPrintJob> mJobs = new CopyOnWriteArrayList<>();
     private LocalPrintJob mCurrent;
 
     /** Queue a print job for printing at the next available opportunity */
@@ -33,6 +36,26 @@
         startNextJob();
     }
 
+    /** Cancel any previously queued job for a printer with the supplied ID. */
+    void cancel(PrinterId printerId) {
+        for (LocalPrintJob job : mJobs) {
+            if (printerId.equals(job.getPrintJob().getInfo().getPrinterId())) {
+                cancel(job.getPrintJobId());
+            }
+        }
+
+        if (mCurrent != null && printerId.equals(mCurrent.getPrintJob().getInfo().getPrinterId())) {
+            cancel(mCurrent.getPrintJobId());
+        }
+    }
+
+    /** Restart any blocked job for a printer with this ID. */
+    void restart(PrinterId printerId) {
+        if (mCurrent != null && printerId.equals(mCurrent.getPrintJob().getInfo().getPrinterId())) {
+            mCurrent.restart();
+        }
+    }
+
     /** Cancel a previously queued job */
     void cancel(PrintJobId id) {
         // If a job hasn't started, kill it instantly.
@@ -44,7 +67,7 @@
             }
         }
 
-        if (mCurrent.getPrintJobId().equals(id)) {
+        if (mCurrent != null && mCurrent.getPrintJobId().equals(id)) {
             mCurrent.cancel();
         }
     }
diff --git a/src/com/android/bips/LocalPrintJob.java b/src/com/android/bips/LocalPrintJob.java
index ba0776c..4a93f18 100644
--- a/src/com/android/bips/LocalPrintJob.java
+++ b/src/com/android/bips/LocalPrintJob.java
@@ -27,6 +27,7 @@
 import com.android.bips.discovery.MdnsDiscovery;
 import com.android.bips.ipp.Backend;
 import com.android.bips.ipp.CapabilitiesCache;
+import com.android.bips.ipp.CertificateStore;
 import com.android.bips.ipp.JobStatus;
 import com.android.bips.jni.BackendConstants;
 import com.android.bips.jni.LocalPrinterCapabilities;
@@ -51,8 +52,9 @@
     private static final int STATE_DISCOVERY = 1;
     private static final int STATE_CAPABILITIES = 2;
     private static final int STATE_DELIVERING = 3;
-    private static final int STATE_CANCEL = 4;
-    private static final int STATE_DONE = 5;
+    private static final int STATE_CERTIFICATE = 4;
+    private static final int STATE_CANCEL = 5;
+    private static final int STATE_DONE = 6;
 
     private final BuiltInPrintService mPrintService;
     private final PrintJob mPrintJob;
@@ -63,6 +65,7 @@
     private Uri mPath;
     private DelayedAction mDiscoveryTimeout;
     private P2pPrinterConnection mConnection;
+    private LocalPrinterCapabilities mCapabilities;
 
     /**
      * Construct the object; use {@link #start(Consumer)} to begin job processing.
@@ -107,12 +110,24 @@
         mPrintService.getDiscovery().start(this);
     }
 
+    /**
+     * Restart the job if possible.
+     */
+    void restart() {
+        if (DEBUG) Log.d(TAG, "restart() " + mPrintJob + " in state " + mState);
+        if (mState == STATE_CERTIFICATE) {
+            mCapabilities.certificate = mPrintService.getCertificateStore().get(mCapabilities.uuid);
+            deliver();
+        }
+    }
+
     void cancel() {
         if (DEBUG) Log.d(TAG, "cancel() " + mPrintJob + " in state " + mState);
 
         switch (mState) {
             case STATE_DISCOVERY:
             case STATE_CAPABILITIES:
+            case STATE_CERTIFICATE:
                 // Cancel immediately
                 mState = STATE_CANCEL;
                 finish(false, null);
@@ -156,6 +171,14 @@
         mPrintService.getDiscovery().stop(this);
         mState = STATE_CAPABILITIES;
         mPath = printer.path;
+        // Upgrade to IPPS path if present
+        for (Uri path : printer.paths) {
+            if (path.getScheme().equals("ipps")) {
+                mPath = path;
+                break;
+            }
+        }
+
         mPrintService.getCapabilitiesCache().request(printer, true, this);
     }
 
@@ -213,14 +236,30 @@
             if (mDiscoveryTimeout != null) {
                 mDiscoveryTimeout.cancel();
             }
-            mState = STATE_DELIVERING;
-            mPrintJob.start();
-            mBackend.print(mPath, mPrintJob, capabilities, this::handleJobStatus);
+            mCapabilities = capabilities;
+            deliver();
         }
     }
 
+    private void deliver() {
+        mState = STATE_DELIVERING;
+        mPrintJob.start();
+        mBackend.print(mPath, mPrintJob, mCapabilities, this::handleJobStatus);
+    }
+
     private void handleJobStatus(JobStatus jobStatus) {
         if (DEBUG) Log.d(TAG, "onJobStatus() " + jobStatus);
+
+        byte[] certificate = jobStatus.getCertificate();
+        if (certificate != null && mCapabilities != null) {
+            CertificateStore store = mPrintService.getCertificateStore();
+            // If there is no certificate, record this one
+            if (store.get(mCapabilities.uuid) == null) {
+                if (DEBUG) Log.d(TAG, "Recording new certificate");
+                store.put(mCapabilities.uuid, certificate);
+            }
+        }
+
         switch (jobStatus.getJobState()) {
             case BackendConstants.JOB_STATE_DONE:
                 switch (jobStatus.getJobResult()) {
@@ -236,7 +275,11 @@
                         break;
                     default:
                         // Job failed
-                        finish(false, null);
+                        if (jobStatus.getBlockedReasonId() == R.string.printer_bad_certificate) {
+                            handleBadCertificate(jobStatus);
+                        } else {
+                            finish(false, null);
+                        }
                         break;
                 }
                 break;
@@ -260,6 +303,20 @@
         }
     }
 
+    private void handleBadCertificate(JobStatus jobStatus) {
+        byte[] certificate = jobStatus.getCertificate();
+
+        if (certificate == null) {
+            mPrintJob.fail(mPrintService.getString(R.string.printer_bad_certificate));
+        } else {
+            if (DEBUG) Log.d(TAG, "Certificate change detected.");
+            mState = STATE_CERTIFICATE;
+            mPrintJob.block(mPrintService.getString(R.string.printer_bad_certificate));
+            mPrintService.notifyCertificateChange(mCapabilities.name,
+                    mPrintJob.getInfo().getPrinterId(), mCapabilities.uuid, certificate);
+        }
+    }
+
     /**
      * Terminate the job, issuing appropriate notifications.
      *
diff --git a/src/com/android/bips/LocalPrinter.java b/src/com/android/bips/LocalPrinter.java
index b8f3da6..f13f3de 100644
--- a/src/com/android/bips/LocalPrinter.java
+++ b/src/com/android/bips/LocalPrinter.java
@@ -109,7 +109,7 @@
         PrinterInfo.Builder builder = new PrinterInfo.Builder(
                 mPrinterId, printer.name,
                 idle ? PrinterInfo.STATUS_IDLE : PrinterInfo.STATUS_UNAVAILABLE)
-                .setIconResourceId(R.drawable.ic_printer)
+                .setIconResourceId(mPrintService.getIconId(printer))
                 .setDescription(mPrintService.getDescription(mDiscoveredPrinter));
 
         if (mCapabilities != null) {
diff --git a/src/com/android/bips/discovery/DiscoveredPrinter.java b/src/com/android/bips/discovery/DiscoveredPrinter.java
index 5d452af..6a135b8 100644
--- a/src/com/android/bips/discovery/DiscoveredPrinter.java
+++ b/src/com/android/bips/discovery/DiscoveredPrinter.java
@@ -26,6 +26,8 @@
 
 import java.io.IOException;
 import java.io.StringWriter;
+import java.util.Collections;
+import java.util.List;
 import java.util.Objects;
 
 /** Represents a network-visible printer */
@@ -42,6 +44,9 @@
     /** Resource path at which the print service can be reached */
     public final Uri path;
 
+    /** All paths at which this this printer can be reached. Includes "path". */
+    public final List<Uri> paths;
+
     /** Lazily-created printer id. */
     private PrinterId mPrinterId;
 
@@ -50,14 +55,27 @@
      *
      * @param uuid     Unique identification of the network printer, if known
      * @param name     Self-identified printer or service name
+     * @param paths    One or more network paths at which the printer is currently available
+     * @param location Self-advertised location of the printer, if known
+     */
+    public DiscoveredPrinter(Uri uuid, String name, List<Uri> paths, String location) {
+        this.uuid = uuid;
+        this.name = name;
+        this.path = paths.get(0);
+        this.paths = Collections.unmodifiableList(paths);
+        this.location = location;
+    }
+
+    /**
+     * Construct minimal information about a network printer
+     *
+     * @param uuid     Unique identification of the network printer, if known
+     * @param name     Self-identified printer or service name
      * @param path     Network path at which the printer is currently available
      * @param location Self-advertised location of the printer, if known
      */
     public DiscoveredPrinter(Uri uuid, String name, Uri path, String location) {
-        this.uuid = uuid;
-        this.name = name;
-        this.path = path;
-        this.location = location;
+        this(uuid, name, Collections.singletonList(path), location);
     }
 
     /** Construct an object based on field values of an JSON object found next in the JsonReader */
@@ -91,6 +109,7 @@
         this.uuid = uuid;
         this.name = printerName;
         this.path = path;
+        this.paths = Collections.singletonList(path);
         this.location = location;
     }
 
@@ -103,6 +122,18 @@
     }
 
     /**
+     * Return true if this printer has a secure (encrypted) path.
+     */
+    public boolean isSecure() {
+        for (Uri path : paths) {
+            if (path.getScheme().equals("ipps")) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
      * Return a host string for the user to see (an IP address or hostname without port number)
      */
     public String getHost() {
@@ -139,7 +170,7 @@
         DiscoveredPrinter other = (DiscoveredPrinter) obj;
         return Objects.equals(uuid, other.uuid)
                 && Objects.equals(name, other.name)
-                && Objects.equals(path, other.path)
+                && Objects.equals(paths, other.paths)
                 && Objects.equals(location, other.location);
     }
 
@@ -148,7 +179,7 @@
         int result = 17;
         result = 31 * result + name.hashCode();
         result = 31 * result + (uuid != null ? uuid.hashCode() : 0);
-        result = 31 * result + path.hashCode();
+        result = 31 * result + paths.hashCode();
         result = 31 * result + (location != null ? location.hashCode() : 0);
         return result;
     }
diff --git a/src/com/android/bips/discovery/MultiDiscovery.java b/src/com/android/bips/discovery/MultiDiscovery.java
index d564db9..93f68b4 100644
--- a/src/com/android/bips/discovery/MultiDiscovery.java
+++ b/src/com/android/bips/discovery/MultiDiscovery.java
@@ -44,31 +44,50 @@
         mChildListener = new Listener() {
             @Override
             public void onPrinterFound(DiscoveredPrinter printer) {
-                printerFound(first(printer.getUri()));
+                printerFound(merged(printer.getUri()));
             }
 
             @Override
             public void onPrinterLost(DiscoveredPrinter printer) {
                 // Merge remaining printer records, if any
-                DiscoveredPrinter first = first(printer.getUri());
-                if (first == null) {
+                DiscoveredPrinter remaining = merged(printer.getUri());
+                if (remaining == null) {
                     printerLost(printer.getUri());
                 } else {
-                    printerFound(first);
+                    printerFound(remaining);
                 }
             }
         };
     }
 
-    /** For a given URI return the first matching record, based on discovery mechanism order */
-    private DiscoveredPrinter first(Uri printerUri) {
+    /**
+     * For a given URI combine and return records with the same printerUri, based on discovery
+     * mechanism order.
+     */
+    private DiscoveredPrinter merged(Uri printerUri) {
+        DiscoveredPrinter combined = null;
+
         for (Discovery discovery : getChildren()) {
-            DiscoveredPrinter found = discovery.getPrinter(printerUri);
-            if (found != null) {
-                return found;
+            DiscoveredPrinter discovered = discovery.getPrinter(printerUri);
+            if (discovered != null) {
+                if (combined == null) {
+                    combined = discovered;
+                } else {
+                    // Merge all paths found, in order, without duplicates
+                    List<Uri> allPaths = new ArrayList<>(combined.paths);
+                    for (Uri path : discovered.paths) {
+                        if (!allPaths.contains(path)) {
+                            allPaths.add(path);
+                        }
+                    }
+                    // Assemble a new printer containing paths.
+                    combined = new DiscoveredPrinter(discovered.uuid, discovered.name, allPaths,
+                            discovered.location);
+                }
             }
         }
-        return null;
+
+        return combined;
     }
 
     @Override
diff --git a/src/com/android/bips/ipp/Backend.java b/src/com/android/bips/ipp/Backend.java
index 94d6a11..0e4affd 100644
--- a/src/com/android/bips/ipp/Backend.java
+++ b/src/com/android/bips/ipp/Backend.java
@@ -191,6 +191,10 @@
 
             builder.setId(params.jobId);
 
+            if (params.certificate != null) {
+                builder.setCertificate(params.certificate);
+            }
+
             if (!TextUtils.isEmpty(params.printerState)) {
                 updateBlockedReasons(builder, params);
             } else if (!TextUtils.isEmpty(params.jobState)) {
diff --git a/src/com/android/bips/ipp/CapabilitiesCache.java b/src/com/android/bips/ipp/CapabilitiesCache.java
index dd4d258..7c361f1 100644
--- a/src/com/android/bips/ipp/CapabilitiesCache.java
+++ b/src/com/android/bips/ipp/CapabilitiesCache.java
@@ -1,6 +1,5 @@
 /*
  * Copyright (C) 2016 The Android Open Source Project
- * Copyright (C) 2016 Mopria Alliance, Inc.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -23,7 +22,6 @@
 import android.net.NetworkInfo;
 import android.net.Uri;
 import android.net.wifi.p2p.WifiP2pManager;
-import android.os.AsyncTask;
 import android.text.TextUtils;
 import android.util.Log;
 import android.util.LruCache;
@@ -48,8 +46,7 @@
  * with the ability to fetch them on cache misses. {@link #close} must be called when use
  * is complete.
  */
-public class CapabilitiesCache extends LruCache<Uri, LocalPrinterCapabilities> implements
-        AutoCloseable {
+public class CapabilitiesCache implements AutoCloseable {
     private static final String TAG = CapabilitiesCache.class.getSimpleName();
     private static final boolean DEBUG = false;
 
@@ -66,6 +63,9 @@
     // Maximum time per retry before giving up on second pass. Must differ from FIRST_PASS_TIMEOUT.
     private static final int SECOND_PASS_TIMEOUT = 8000;
 
+    // Underlying cache
+    private final LruCache<Uri, LocalPrinterCapabilities> mCache = new LruCache<>(CACHE_SIZE);
+
     // Outstanding requests based on printer path
     private final Map<Uri, Request> mRequests = new HashMap<>();
     private final Set<Uri> mToEvict = new HashSet<>();
@@ -81,7 +81,6 @@
      * @param maxConcurrent Maximum number of capabilities requests to make at any one time
      */
     public CapabilitiesCache(BuiltInPrintService service, Backend backend, int maxConcurrent) {
-        super(CACHE_SIZE);
         if (DEBUG) Log.d(TAG, "CapabilitiesCache()");
 
         mService = service;
@@ -96,7 +95,7 @@
                     // Evict specified device capabilities when P2P network is lost.
                     if (DEBUG) Log.d(TAG, "Evicting P2P " + mToEvictP2p);
                     for (Uri uri : mToEvictP2p) {
-                        remove(uri);
+                        mCache.remove(uri);
                     }
                     mToEvictP2p.clear();
                 }
@@ -108,7 +107,7 @@
                 // Evict specified device capabilities when network is lost.
                 if (DEBUG) Log.d(TAG, "Evicting Wi-Fi " + mToEvict);
                 for (Uri uri : mToEvict) {
-                    remove(uri);
+                    mCache.remove(uri);
                 }
                 mToEvict.clear();
             }
@@ -173,7 +172,20 @@
      * Returns capabilities for the specified printer, if known
      */
     public LocalPrinterCapabilities get(DiscoveredPrinter printer) {
-        return get(printer.path);
+        LocalPrinterCapabilities capabilities = mCache.get(printer.path);
+        // Populate certificate from store if possible
+        if (capabilities != null) {
+            capabilities.certificate = mService.getCertificateStore().get(capabilities.uuid);
+        }
+        return capabilities;
+    }
+
+    /**
+     * Remove capabilities corresponding to a Printer URI
+     * @return The removed capabilities, if any
+     */
+    public LocalPrinterCapabilities remove(Uri printerUri) {
+        return mCache.remove(printerUri);
     }
 
     /**
@@ -281,10 +293,11 @@
                     startNextRequest();
                     return;
                 } else {
-                    remove(printer.getUri());
+                    mCache.remove(printer.getUri());
                 }
             } else {
-                put(printer.path, capabilities);
+                capabilities.certificate = mService.getCertificateStore().get(capabilities.uuid);
+                mCache.put(printer.path, capabilities);
             }
 
             LocalPrinterCapabilities result = capabilities;
diff --git a/src/com/android/bips/ipp/CertificateStore.java b/src/com/android/bips/ipp/CertificateStore.java
new file mode 100644
index 0000000..605f711
--- /dev/null
+++ b/src/com/android/bips/ipp/CertificateStore.java
@@ -0,0 +1,182 @@
+/*
+ * 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.
+ */
+
+package com.android.bips.ipp;
+
+import android.util.JsonReader;
+import android.util.JsonWriter;
+import android.util.Log;
+
+import com.android.bips.BuiltInPrintService;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * A persistent cache of certificate public keys known to be associated with certain printer
+ * UUIDs.
+ */
+public class CertificateStore {
+    private static final String TAG = CertificateStore.class.getSimpleName();
+    private static final boolean DEBUG = false;
+
+    /** File location of the on-disk certificate store. */
+    private final File mStoreFile;
+
+    /** RAM-based store of certificates (UUID to certificate) */
+    private final Map<String, byte[]> mCertificates = new HashMap<>();
+
+    public CertificateStore(BuiltInPrintService service) {
+        mStoreFile = new File(service.getCacheDir(), getClass().getSimpleName() + ".json");
+        load();
+    }
+
+    /** Write a new, non-null certificate to the store. */
+    public void put(String uuid, byte[] certificate) {
+        byte[] oldCertificate = mCertificates.put(uuid, certificate);
+        if (oldCertificate == null || !Arrays.equals(oldCertificate, certificate)) {
+            // Cache the certificate for later
+            if (DEBUG) Log.d(TAG, "New certificate uuid=" + uuid + " len=" + certificate.length);
+            save();
+        }
+    }
+
+    /** Return the known certificate public key for a printer having the specified UUID, or null. */
+    public byte[] get(String uuid) {
+        return mCertificates.get(uuid);
+    }
+
+    /** Write to storage immediately. */
+    private void save() {
+        if (mStoreFile.exists()) {
+            mStoreFile.delete();
+        }
+
+        try (JsonWriter writer = new JsonWriter(new BufferedWriter(new FileWriter(mStoreFile)))) {
+            writer.beginObject();
+            writer.name("certificates");
+            writer.beginArray();
+            for (Map.Entry<String, byte[]> entry : mCertificates.entrySet()) {
+                writer.beginObject();
+                writer.name("uuid").value(entry.getKey());
+                writer.name("pubkey").value(bytesToHex(entry.getValue()));
+                writer.endObject();
+            }
+            writer.endArray();
+            writer.endObject();
+            if (DEBUG) Log.d(TAG, "Wrote " + mCertificates.size() + " certificates to store");
+        } catch (NullPointerException | IOException e) {
+            Log.w(TAG, "Error while storing to " + mStoreFile, e);
+        }
+    }
+
+    /** Load known certificates from storage into RAM. */
+    private void load() {
+        if (!mStoreFile.exists()) {
+            return;
+        }
+
+        try (JsonReader reader = new JsonReader(new BufferedReader(new FileReader(mStoreFile)))) {
+            reader.beginObject();
+            while (reader.hasNext()) {
+                String itemName = reader.nextName();
+                if (itemName.equals("certificates")) {
+                    reader.beginArray();
+                    while (reader.hasNext()) {
+                        loadItem(reader);
+                    }
+                    reader.endArray();
+                } else {
+                    reader.skipValue();
+                }
+            }
+            reader.endObject();
+        } catch (IllegalStateException | IOException error) {
+            Log.w(TAG, "Error while loading from " + mStoreFile, error);
+        }
+        if (DEBUG) Log.d(TAG, "Loaded size=" + mCertificates.size() + " from " + mStoreFile);
+    }
+
+    /** Load a single certificate entry into RAM. */
+    private void loadItem(JsonReader reader) throws IOException {
+        String uuid = null;
+        byte[] pubkey = null;
+        reader.beginObject();
+        while (reader.hasNext()) {
+            String itemName = reader.nextName();
+            switch(itemName) {
+                case "uuid":
+                    uuid = reader.nextString();
+                    break;
+                case "pubkey":
+                    try {
+                        pubkey = hexToBytes(reader.nextString());
+                    } catch (IllegalArgumentException ignored) {
+                    }
+                    break;
+                default:
+                    reader.skipValue();
+            }
+        }
+        reader.endObject();
+        if (uuid != null && pubkey != null) {
+            mCertificates.put(uuid, pubkey);
+        }
+    }
+
+    private static final char[] HEX_CHARS = "0123456789ABCDEF".toCharArray();
+
+    /** Converts a byte array to a hexadecimal string, or null if bytes are null. */
+    private static String bytesToHex(byte[] bytes) {
+        if (bytes == null) {
+            return null;
+        }
+
+        char[] hexChars = new char[bytes.length * 2];
+        for (int i = 0; i < bytes.length; i++) {
+            int b = bytes[i] & 0xFF;
+            hexChars[i * 2] = HEX_CHARS[b >>> 4];
+            hexChars[i * 2 + 1] = HEX_CHARS[b & 0x0F];
+        }
+        return new String(hexChars);
+    }
+
+    /** Converts a hexadecimal string to a byte array, or null if hexString is null. */
+    private static byte[] hexToBytes(String hexString) {
+        if (hexString == null) {
+            return null;
+        }
+
+        char[] source = hexString.toCharArray();
+        byte[] dest = new byte[source.length / 2];
+        for (int sourcePos = 0, destPos = 0; sourcePos < source.length; ) {
+            int hi = Character.digit(source[sourcePos++], 16);
+            int lo = Character.digit(source[sourcePos++], 16);
+            if ((hi < 0) || (lo < 0)) {
+                throw new IllegalArgumentException();
+            }
+            dest[destPos++] = (byte) (hi << 4 | lo);
+        }
+        return dest;
+    }
+}
diff --git a/src/com/android/bips/ipp/JobStatus.java b/src/com/android/bips/ipp/JobStatus.java
index 72be347..b2d07e2 100644
--- a/src/com/android/bips/ipp/JobStatus.java
+++ b/src/com/android/bips/ipp/JobStatus.java
@@ -53,12 +53,15 @@
                 R.string.printer_low_on_toner);
         sBlockReasonsMap.put(BackendConstants.BLOCKED_REASON__BUSY, R.string.printer_busy);
         sBlockReasonsMap.put(BackendConstants.BLOCKED_REASON__OFFLINE, R.string.printer_offline);
+        sBlockReasonsMap.put(BackendConstants.BLOCKED_REASON__BAD_CERTIFICATE,
+                R.string.printer_bad_certificate);
     }
 
     private int mId;
     private String mJobState;
     private String mJobResult;
     private final Set<String> mBlockedReasons;
+    private byte[] mCertificate;
 
     /** Create a new, blank job status */
     public JobStatus() {
@@ -72,6 +75,7 @@
         mJobState = other.mJobState;
         mJobResult = other.mJobResult;
         mBlockedReasons = other.mBlockedReasons;
+        mCertificate = other.mCertificate;
     }
 
     /** Returns a string resource ID corresponding to a blocked reason, or 0 if none found */
@@ -104,12 +108,18 @@
         return !TextUtils.isEmpty(mJobResult);
     }
 
+    /** Return certificate if supplied as part of this status. */
+    public byte[] getCertificate() {
+        return mCertificate;
+    }
+
     @Override
     public String toString() {
         return "JobStatus{id=" + mId
                 + ", jobState=" + mJobState
                 + ", jobResult=" + mJobResult
                 + ", blockedReasons=" + mBlockedReasons
+                + ", certificate=" + (mCertificate != null)
                 + "}";
     }
 
@@ -139,6 +149,11 @@
             return this;
         }
 
+        Builder setCertificate(byte[] certificate) {
+            mPrototype.mCertificate = certificate;
+            return this;
+        }
+
         Builder clearBlockedReasons() {
             mPrototype.mBlockedReasons.clear();
             return this;
diff --git a/src/com/android/bips/jni/BackendConstants.java b/src/com/android/bips/jni/BackendConstants.java
index a734a46..9960a47 100644
--- a/src/com/android/bips/jni/BackendConstants.java
+++ b/src/com/android/bips/jni/BackendConstants.java
@@ -67,5 +67,6 @@
     public static final String BLOCKED_REASON__LOW_ON_INK = "marker-ink-almost-empty";
     public static final String BLOCKED_REASON__LOW_ON_TONER = "marker-toner-almost-empty";
     public static final String BLOCKED_REASON__REALLY_LOW_ON_INK = "marker-ink-really-low";
+    public static final String BLOCKED_REASON__BAD_CERTIFICATE = "bad-certificate";
     public static final String BLOCKED_REASON__UNKNOWN = "unknown";
 }
diff --git a/src/com/android/bips/jni/JobCallbackParams.java b/src/com/android/bips/jni/JobCallbackParams.java
index 97badec..f0b9aee 100644
--- a/src/com/android/bips/jni/JobCallbackParams.java
+++ b/src/com/android/bips/jni/JobCallbackParams.java
@@ -24,4 +24,5 @@
     public String jobState;
     public String jobDoneResult;
     public String[] blockedReasons;
+    public byte[] certificate;
 }
diff --git a/src/com/android/bips/jni/LocalPrinterCapabilities.java b/src/com/android/bips/jni/LocalPrinterCapabilities.java
index f30b24a..332ce5d 100644
--- a/src/com/android/bips/jni/LocalPrinterCapabilities.java
+++ b/src/com/android/bips/jni/LocalPrinterCapabilities.java
@@ -54,6 +54,9 @@
     /** Bears the underlying native C structure (printer_capabilities_t) or null if not present */
     public byte[] nativeData;
 
+    /** Public key of certificate for this printer, if known */
+    public byte[] certificate;
+
     public void buildCapabilities(BuiltInPrintService service,
             PrinterCapabilitiesInfo.Builder builder) {
         builder.setColorModes(
@@ -122,6 +125,7 @@
                 + " supportedMediaTypes=" + Arrays.toString(supportedMediaTypes)
                 + " supportedMediaSizes=" + Arrays.toString(supportedMediaSizes)
                 + " inetAddress=" + inetAddress
+                + " certificate=" + (certificate != null)
                 + "}";
     }
 }
diff --git a/src/com/android/bips/ui/PrinterPreference.java b/src/com/android/bips/ui/PrinterPreference.java
index 36cce3c..9b048b4 100644
--- a/src/com/android/bips/ui/PrinterPreference.java
+++ b/src/com/android/bips/ui/PrinterPreference.java
@@ -47,7 +47,7 @@
         }
 
         setSummary(mPrintService.getDescription(printer));
-        setIcon(R.drawable.ic_printer);
+        setIcon(mPrintService.getIconId(printer));
     }
 
     DiscoveredPrinter getPrinter() {