diff --git a/src/main/java/app/attestation/server/AttestationProtocol.java b/src/main/java/app/attestation/server/AttestationProtocol.java index ac644e58..b095df0d 100644 --- a/src/main/java/app/attestation/server/AttestationProtocol.java +++ b/src/main/java/app/attestation/server/AttestationProtocol.java @@ -78,7 +78,7 @@ class AttestationProtocol { // byte[] compressedChain { [short encodedCertificateLength, byte[] encodedCertificate] } // byte[] fingerprint (length: FINGERPRINT_LENGTH) // int osEnforcedFlags - // short autoRebootMinutes (-1 for unknown) + // int autoRebootSeconds (-1 for unknown) // byte portSecurityMode (-1 for unknown) // byte userCount (-1 for unknown) // } @@ -86,7 +86,7 @@ class AttestationProtocol { // // Protocol version changes: // - // 6: autoRebootMinutes added + // 6: autoRebootSeconds added // 6: portSecurityMode added // 6: userCount added // @@ -128,7 +128,7 @@ class AttestationProtocol { // the outer signature and the rest of the chain for pinning the expected chain. It enforces // downgrade protection for the OS version/patch (bootloader/TEE enforced) and app version (OS // enforced) by keeping them updated. - static final byte PROTOCOL_VERSION = 5; + static final byte PROTOCOL_VERSION = 6; private static final byte PROTOCOL_VERSION_MINIMUM = 5; // can become longer in the future, but this is the minimum length private static final byte CHALLENGE_MESSAGE_LENGTH = 1 + RANDOM_TOKEN_LENGTH * 2; @@ -219,7 +219,12 @@ class AttestationProtocol { public record DeviceInfo(String name, int attestationVersion, int keymasterVersion, // API for detecting this was replaced in keymaster v3 but the new one isn't used yet boolean rollbackResistant, - boolean enforceStrongBox, String osName) {} + boolean enforceStrongBox, String osName) { + + boolean hasPogoPins() { + return DEVICE_PIXEL_TABLET.equals(name); + } + } private static final ImmutableSet extraPatchLevelMissing = ImmutableSet.of( DEVICE_SM_G970F, @@ -474,7 +479,12 @@ private static byte[] getFingerprint(final Certificate certificate) private record Verified(String device, String verifiedBootKey, byte[] verifiedBootHash, String osName, int osVersion, int osPatchLevel, int vendorPatchLevel, int bootPatchLevel, - int appVersion, int appVariant, int securityLevel, boolean attestKey) {} + int appVersion, int appVariant, int securityLevel, boolean attestKey) { + + boolean hasPogoPins() { + return DEVICE_PIXEL_TABLET.equals(device); + } + } private static X509Certificate generateCertificate(final InputStream in) throws CertificateException { @@ -807,6 +817,13 @@ private static String toYesNoString(final boolean value) { return value ? "yes" : "no"; } + record SecurityStateExt(int autoRebootSeconds, byte portSecurityMode, byte userCount) { + static int UNKNOWN_VALUE = -1; + static int INVALID_VALUE = -2; + static SecurityStateExt UNKNOWN = new SecurityStateExt( + (short) UNKNOWN_VALUE, (byte) UNKNOWN_VALUE, (byte) UNKNOWN_VALUE); + } + private static void verify(final byte[] fingerprint, final Cache pendingChallenges, final long userId, final boolean paired, final ByteBuffer signedMessage, final byte[] signature, @@ -814,7 +831,8 @@ private static void verify(final byte[] fingerprint, final boolean accessibility, final boolean deviceAdmin, final boolean deviceAdminNonSystem, final boolean adbEnabled, final boolean addUsersWhenLocked, final boolean enrolledBiometrics, - final boolean oemUnlockAllowed, final boolean systemUser) + final boolean oemUnlockAllowed, final boolean systemUser, + final SecurityStateExt securityStateExt) throws GeneralSecurityException, IOException, SQLiteException { final String fingerprintHex = BaseEncoding.base16().encode(fingerprint); final byte[] currentFingerprint = getFingerprint(attestationCertificates[0]); @@ -952,6 +970,9 @@ private static void verify(final byte[] fingerprint, addUsersWhenLocked = ?, oemUnlockAllowed = ?, systemUser = ?, + autoRebootSeconds = ?, + portSecurityMode = ?, + userCount = ?, verifiedTimeLast = ? WHERE fingerprint = ?"""); try { @@ -974,8 +995,11 @@ private static void verify(final byte[] fingerprint, update.bind(13, addUsersWhenLocked ? 1 : 0); update.bind(14, oemUnlockAllowed ? 1 : 0); update.bind(15, systemUser ? 1 : 0); - update.bind(16, now); - update.bind(17, fingerprint); + update.bind(16, securityStateExt.autoRebootSeconds); + update.bind(17, securityStateExt.portSecurityMode); + update.bind(18, securityStateExt.userCount); + update.bind(19, now); + update.bind(20, fingerprint); update.step(); } finally { update.dispose(); @@ -1005,10 +1029,13 @@ INSERT INTO Devices ( addUsersWhenLocked, oemUnlockAllowed, systemUser, + autoRebootSeconds, + portSecurityMode, + userCount, verifiedTimeFirst, verifiedTimeLast, userId - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"""); + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"""); try { insert.bind(1, fingerprint); insert.bind(2, encodeChain(DEFLATE_DICTIONARY_4, attestationCertificates)); @@ -1034,9 +1061,12 @@ INSERT INTO Devices ( insert.bind(18, addUsersWhenLocked ? 1 : 0); insert.bind(19, oemUnlockAllowed ? 1 : 0); insert.bind(20, systemUser ? 1 : 0); - insert.bind(21, now); - insert.bind(22, now); - insert.bind(23, userId); + insert.bind(21, securityStateExt.autoRebootSeconds); + insert.bind(22, securityStateExt.portSecurityMode); + insert.bind(23, securityStateExt.userCount); + insert.bind(24, now); + insert.bind(25, now); + insert.bind(26, userId); insert.step(); } finally { insert.dispose(); @@ -1061,8 +1091,11 @@ INSERT INTO Attestations ( adbEnabled, addUsersWhenLocked, oemUnlockAllowed, - systemUser - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"""); + systemUser, + autoRebootSeconds, + portSecurityMode, + userCount + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"""); try { insert.bind(1, fingerprint); insert.bind(2, now); @@ -1085,6 +1118,9 @@ INSERT INTO Attestations ( insert.bind(15, addUsersWhenLocked ? 1 : 0); insert.bind(16, oemUnlockAllowed ? 1 : 0); insert.bind(17, systemUser ? 1 : 0); + insert.bind(18, securityStateExt.autoRebootSeconds); + insert.bind(19, securityStateExt.portSecurityMode); + insert.bind(20, securityStateExt.userCount); insert.step(); } finally { @@ -1189,10 +1225,14 @@ static void verifySerialized(final byte[] attestationResult, throw new GeneralSecurityException("invalid device administrator state"); } + SecurityStateExt securityStateExt; if (version >= 6) { - final short autoRebootMinutes = deserializer.getShort(); + final int autoRebootSeconds = deserializer.getInt(); final byte portSecurityMode = deserializer.get(); final byte userCount = deserializer.get(); + securityStateExt = new SecurityStateExt(autoRebootSeconds, portSecurityMode, userCount); + } else { + securityStateExt = SecurityStateExt.UNKNOWN; } final int signatureLength = deserializer.remaining(); @@ -1204,6 +1244,7 @@ static void verifySerialized(final byte[] attestationResult, verify(fingerprint, pendingChallenges, userId, paired, deserializer.asReadOnlyBuffer(), signature, certificates, userProfileSecure, accessibility, deviceAdmin, deviceAdminNonSystem, - adbEnabled, addUsersWhenLocked, enrolledBiometrics, oemUnlockAllowed, systemUser); + adbEnabled, addUsersWhenLocked, enrolledBiometrics, oemUnlockAllowed, systemUser, + securityStateExt); } } diff --git a/src/main/java/app/attestation/server/AttestationServer.java b/src/main/java/app/attestation/server/AttestationServer.java index cf5a9902..bfb27024 100644 --- a/src/main/java/app/attestation/server/AttestationServer.java +++ b/src/main/java/app/attestation/server/AttestationServer.java @@ -234,6 +234,9 @@ adbEnabled INTEGER NOT NULL CHECK (adbEnabled in (0, 1)), addUsersWhenLocked INTEGER NOT NULL CHECK (addUsersWhenLocked in (0, 1)), oemUnlockAllowed INTEGER NOT NULL CHECK (oemUnlockAllowed in (0, 1)), systemUser INTEGER NOT NULL CHECK (systemUser in (0, 1)), + autoRebootSeconds INTEGER NOT NULL CHECK (autoRebootSeconds in (-2, -1) OR autoRebootSeconds >= 0), + portSecurityMode INTEGER NOT NULL CHECK (portSecurityMode in (-2, -1) OR portSecurityMode >= 0), + userCount INTEGER NOT NULL CHECK (userCount in (-2, -1) OR userCount >= 1), verifiedTimeFirst INTEGER NOT NULL, verifiedTimeLast INTEGER NOT NULL, expiredTimeLast INTEGER, @@ -261,7 +264,10 @@ deviceAdmin INTEGER NOT NULL CHECK (deviceAdmin in (0, 1, 2)), adbEnabled INTEGER NOT NULL CHECK (adbEnabled in (0, 1)), addUsersWhenLocked INTEGER NOT NULL CHECK (addUsersWhenLocked in (0, 1)), oemUnlockAllowed INTEGER NOT NULL CHECK (oemUnlockAllowed in (0, 1)), - systemUser INTEGER NOT NULL CHECK (systemUser in (0, 1)) + systemUser INTEGER NOT NULL CHECK (systemUser in (0, 1)), + autoRebootSeconds INTEGER NOT NULL CHECK (autoRebootSeconds in (-2, -1) OR autoRebootSeconds >= 0), + portSecurityMode INTEGER NOT NULL CHECK (portSecurityMode in (-2, -1) OR portSecurityMode >= 0), + userCount INTEGER NOT NULL CHECK (userCount in (-2, -1) OR userCount >= 1) ) STRICT"""; private static final String CREATE_ATTESTATION_INDICES = """ @@ -575,6 +581,140 @@ INSERT INTO Attestations ( logger.info("Migrated to schema version: " + userVersion); } + // add failureAlertTime column to Devices + targetUserVersion = 15; + if (userVersion < targetUserVersion) { + conn.exec("PRAGMA foreign_keys = OFF"); + conn.exec("BEGIN IMMEDIATE TRANSACTION"); + + conn.exec("ALTER TABLE Devices RENAME TO OldDevices"); + conn.exec("ALTER TABLE Attestations RENAME TO OldAttestations"); + + conn.exec(CREATE_ATTESTATION_TABLES); + + conn.exec(""" + INSERT INTO Devices ( + fingerprint, + pinnedCertificates, + attestKey, + pinnedVerifiedBootKey, + verifiedBootHash, + pinnedOsVersion, + pinnedOsPatchLevel, + pinnedVendorPatchLevel, + pinnedBootPatchLevel, + pinnedAppVersion, + pinnedAppVariant, + pinnedSecurityLevel, + userProfileSecure, + enrolledBiometrics, + accessibility, + deviceAdmin, + adbEnabled, + addUsersWhenLocked, + oemUnlockAllowed, + systemUser, + autoRebootSeconds, + portSecurityMode, + userCount, + verifiedTimeFirst, + verifiedTimeLast, + expiredTimeLast, + failureTimeLast, + failureAlertTime, + userId, + deletionTime) + SELECT + fingerprint, + pinnedCertificates, + attestKey, + pinnedVerifiedBootKey, + verifiedBootHash, + pinnedOsVersion, + pinnedOsPatchLevel, + pinnedVendorPatchLevel, + pinnedBootPatchLevel, + pinnedAppVersion, + pinnedAppVariant, + pinnedSecurityLevel, + userProfileSecure, + enrolledBiometrics, + accessibility, + deviceAdmin, + adbEnabled, + addUsersWhenLocked, + oemUnlockAllowed, + systemUser, + -1, + -1, + -1, + verifiedTimeFirst, + verifiedTimeLast, + expiredTimeLast, + failureTimeLast, + failureAlertTime, + userId, + deletionTime + FROM OldDevices"""); + + conn.exec(""" + INSERT INTO Attestations ( + id, + fingerprint, + time, + strong, + osVersion, + osPatchLevel, + vendorPatchLevel, + bootPatchLevel, + verifiedBootHash, + appVersion, + userProfileSecure, + enrolledBiometrics, + accessibility, + deviceAdmin, + adbEnabled, + addUsersWhenLocked, + oemUnlockAllowed, + systemUser, + autoRebootSeconds, + portSecurityMode, + userCount + ) SELECT + id, + fingerprint, + time, + strong, + osVersion, + osPatchLevel, + vendorPatchLevel, + bootPatchLevel, + verifiedBootHash, + appVersion, + userProfileSecure, + enrolledBiometrics, + accessibility, + deviceAdmin, + adbEnabled, + addUsersWhenLocked, + oemUnlockAllowed, + systemUser, + -1, + -1, + -1 + FROM OldAttestations"""); + + conn.exec("DROP TABLE OldDevices"); + conn.exec("DROP TABLE OldAttestations"); + + conn.exec(CREATE_ATTESTATION_INDICES); + conn.exec("PRAGMA user_version = " + targetUserVersion); + conn.exec("COMMIT TRANSACTION"); + userVersion = targetUserVersion; + conn.exec("PRAGMA foreign_keys = ON"); + logger.info("Migrated to schema version: " + userVersion); + } + logger.info("Finished database setup for " + ATTESTATION_DATABASE); } finally { conn.dispose(); @@ -1392,6 +1532,9 @@ private static void writeDevicesJson(final HttpExchange exchange, final long use addUsersWhenLocked, oemUnlockAllowed, systemUser, + autoRebootSeconds, + portSecurityMode, + userCount, verifiedTimeFirst, verifiedTimeLast, (SELECT min(id) FROM Attestations WHERE Attestations.fingerprint = Devices.fingerprint), @@ -1460,10 +1603,14 @@ private static void writeDevicesJson(final HttpExchange exchange, final long use device.add("addUsersWhenLocked", select.columnInt(17)); device.add("oemUnlockAllowed", select.columnInt(18)); device.add("systemUser", select.columnInt(19)); - device.add("verifiedTimeFirst", select.columnLong(20)); - device.add("verifiedTimeLast", select.columnLong(21)); - device.add("minId", select.columnLong(22)); - device.add("maxId", select.columnLong(23)); + device.add("autoRebootSeconds", select.columnInt(20)); + device.add("portSecurityMode", select.columnInt(21)); + device.add("userCount", select.columnInt(22)); + device.add("verifiedTimeFirst", select.columnLong(23)); + device.add("verifiedTimeLast", select.columnLong(24)); + device.add("minId", select.columnLong(25)); + device.add("maxId", select.columnLong(26)); + device.add("hasPogoPins", info.hasPogoPins() ? 1 : 0); devices.add(device); } } finally { @@ -1533,7 +1680,10 @@ private static void writeAttestationHistoryJson(final HttpExchange exchange, fin Attestations.adbEnabled, Attestations.addUsersWhenLocked, Attestations.oemUnlockAllowed, - Attestations.systemUser + Attestations.systemUser, + Attestations.autoRebootSeconds, + Attestations.portSecurityMode, + Attestations.userCount FROM Attestations INNER JOIN Devices ON Attestations.fingerprint = Devices.fingerprint WHERE Devices.fingerprint = ? AND userid = ? @@ -1568,6 +1718,9 @@ private static void writeAttestationHistoryJson(final HttpExchange exchange, fin attestation.add("addUsersWhenLocked", history.columnInt(14)); attestation.add("oemUnlockAllowed", history.columnInt(15)); attestation.add("systemUser", history.columnInt(16)); + attestation.add("autoRebootSeconds", history.columnInt(17)); + attestation.add("portSecurityMode", history.columnInt(18)); + attestation.add("userCount", history.columnInt(19)); attestations.add(attestation); rowCount += 1; } diff --git a/static/monitoring.js b/static/monitoring.js index 2fc661fc..618399c0 100644 --- a/static/monitoring.js +++ b/static/monitoring.js @@ -85,6 +85,75 @@ function toSecurityLevelString(securityLevel, attestKey) { throw new Error("Invalid security level"); } +function autoRebootTimeoutString(autoRebootSeconds) { + if (autoRebootSeconds >= 0) { + const duration = { + hours: Math.floor(autoRebootSeconds / 60 / 60), + minutes: Math.floor(autoRebootSeconds / 60) % 60, + seconds: autoRebootSeconds % 60, + }; + if (typeof window.Intl.DurationFormat == "function") { + const durationFormat = new Intl.DurationFormat("en", {style: "long"}); + return durationFormat.format(duration); + } else { + let durationString = ""; + if (duration.hours > 0) { + durationString += duration.hours + " hour" + ((duration.hours > 1 ? "s" : "")); + } + + if (duration.minutes > 0) { + if (durationString.length > 0) { + durationString += ", "; + } + durationString += duration.minutes + " minute" + ((duration.minutes > 1 ? "s" : "")); + } + + if (duration.seconds > 0) { + if (durationString.length > 0) { + durationString += ", "; + } + durationString += duration.seconds + " second" + ((duration.seconds > 1 ? "s" : "")); + } + + return durationString; + } + } else if (autoRebootSeconds == -1) { + return "Unknown"; + } else if (autoRebootSeconds == -2) { + return "Invalid"; + } + throw new Error("Invalid auto reboot minutes value"); +} + +function usbPortSecurityModeString(portSecurityMode, hasPogoPins) { + if (portSecurityMode >= 0) { + switch (portSecurityMode) { + case 0: return (hasPogoPins > 0) ? "USB-C port off, pogo pins used only for charging" : "Off"; + case 1: return "Charging-only"; + case 2: return "Charging-only when locked"; + case 3: return "Charging-only when locked, except before first unlock"; + case 4: return "On"; + default: break; + } + } else if (portSecurityMode == -1) { + return "Unknown"; + } else if (portSecurityMode == -2) { + return "Invalid"; + } + throw new Error("Invalid port security mode value"); +} + +function userCountString(userCount) { + if (userCount > 0) { + return userCount + " users"; + } else if (userCount == -1) { + return "Unknown"; + } else if (userCount == -2) { + return "Invalid"; + } + throw new Error("Invalid port security mode value"); +} + function reloadQrCode() { qr.src = "/placeholder.gif"; qr.alt = ""; @@ -131,7 +200,7 @@ function appendLine(element, text) { element.appendChild(document.createElement("br")); } -function fetchHistory(parent, nextOffset) { +function fetchHistory(parent, nextOffset, hasPogoPins) { const parentdata = parent.dataset; parentdata.offsetId = Number(nextOffset); post("/api/attestation-history.json", JSON.stringify({ @@ -177,11 +246,14 @@ function fetchHistory(parent, nextOffset) { appendLine(parent, "Add users from lock screen: " + toYesNoString(attestation.addUsersWhenLocked)); appendLine(parent, "OEM unlocking allowed: " + toYesNoString(attestation.oemUnlockAllowed)); appendLine(parent, "Main user account: " + toYesNoString(attestation.systemUser)); + appendLine(parent, "Auto reboot timeout: " + autoRebootTimeoutString(attestation.autoRebootSeconds)); + appendLine(parent, "USB-C port" + ((hasPogoPins > 0) ? " and pogo pins" : "") + " security mode: " + usbPortSecurityModeString(attestation.portSecurityMode, hasPogoPins)); + appendLine(parent, "User count: " + userCountString(attestation.userCount)); } const earliestCurrentId = attestations.slice(-1)[0].id; function fetchHistoryNextPage() { parent.removeChild(parent.lastChild); - return fetchHistory(parent, earliestCurrentId - 1); + return fetchHistory(parent, earliestCurrentId - 1, hasPogoPins); } if (earliestCurrentId !== Number(parentdata.minId)) { parent.appendChild(create("button", "Load More", "page_history_next")).onclick = fetchHistoryNextPage; @@ -301,6 +373,9 @@ function fetchDevices() { appendLine(info, "Add users from lock screen: " + toYesNoString(device.addUsersWhenLocked)); appendLine(info, "OEM unlocking allowed: " + toYesNoString(device.oemUnlockAllowed)); appendLine(info, "Main user account: " + toYesNoString(device.systemUser)); + appendLine(info, "Auto reboot timeout: " + autoRebootTimeoutString(device.autoRebootSeconds)); + appendLine(info, "USB-C port" + ((device.hasPogoPins > 0) ? " and pogo pins" : "") + " security mode: " + usbPortSecurityModeString(device.portSecurityMode, device.hasPogoPins)); + appendLine(info, "User count: " + userCountString(device.userCount)); info.appendChild(create("h3", "Attestation history")); appendLine(info, "First verified time: " + new Date(device.verifiedTimeFirst)); @@ -312,7 +387,7 @@ function fetchDevices() { history.dataset.maxId = Number(device.maxId); history.hidden = true; // always starts with latest attestation history entry - historyButton.onclick = fetchHistory(history, device.maxId); + historyButton.onclick = fetchHistory(history, device.maxId, device.hasPogoPins); } for (const toggle of document.getElementsByClassName("toggle")) {