mirror of
https://github.com/luckfox-eng29/kvm.git
synced 2026-05-27 16:45:08 +02:00
feat(ota): add OTA signature verification and public key handling
Signed-off-by: luckfox-eng29 <eng29@luckfox.com>
This commit is contained in:
@@ -38,8 +38,8 @@ const appendStatToMap = <T extends { timestamp: number }>(
|
||||
};
|
||||
|
||||
// Constants and types
|
||||
export type AvailableSidebarViews ="ConsoleLogViewer"|"MacroMoreList"|"Fullscreen"|"TerminalTabsMobile"|"SettingsModal"|"ClipboardMobile"|"KeyboardPanel"|"MousePanel"|"SettingsVideo"
|
||||
|"connection-stats"|"Clipboard"|"PowerControl"|"Macros"|"VirtualMedia"|"SharedFolders"|null;
|
||||
export type AvailableSidebarViews = "ConsoleLogViewer" | "MacroMoreList" | "Fullscreen" | "TerminalTabsMobile" | "SettingsModal" | "ClipboardMobile" | "KeyboardPanel" | "MousePanel" | "SettingsVideo"
|
||||
| "connection-stats" | "Clipboard" | "PowerControl" | "Macros" | "VirtualMedia" | "SharedFolders" | "UsbEpModeSelect" | "UsbStatusPanel" | null;
|
||||
export type AvailableTerminalTypes = "kvm" | "serial" | "none";
|
||||
|
||||
export interface User {
|
||||
@@ -189,9 +189,6 @@ interface RTCState {
|
||||
|
||||
serialConsole: RTCDataChannel | null;
|
||||
setSerialConsole: (channel: RTCDataChannel | null) => void;
|
||||
|
||||
hidChannel: RTCDataChannel | null;
|
||||
setHidChannel: (channel: RTCDataChannel | null) => void;
|
||||
}
|
||||
|
||||
export const useRTCStore = create<RTCState>(set => ({
|
||||
@@ -201,9 +198,6 @@ export const useRTCStore = create<RTCState>(set => ({
|
||||
rpcDataChannel: null,
|
||||
setRpcDataChannel: channel => set({ rpcDataChannel: channel }),
|
||||
|
||||
hidChannel: null,
|
||||
setHidChannel: channel => set({ hidChannel: channel }),
|
||||
|
||||
transceiver: null,
|
||||
setTransceiver: transceiver => set({ transceiver }),
|
||||
|
||||
@@ -592,9 +586,6 @@ export interface HidState {
|
||||
keyboardLedStateSyncAvailable: boolean;
|
||||
setKeyboardLedStateSyncAvailable: (available: boolean) => void;
|
||||
|
||||
rpcHidReady: boolean;
|
||||
setRpcHidReady: (ready: boolean) => void;
|
||||
|
||||
keysDownState?: { modifier: number; keys: number[] };
|
||||
setKeysDownState: (state: { modifier: number; keys: number[] }) => void;
|
||||
|
||||
@@ -656,9 +647,6 @@ export const useHidStore = create<HidState>((set, get) => ({
|
||||
set({ keyboardLedState });
|
||||
},
|
||||
|
||||
rpcHidReady: false,
|
||||
setRpcHidReady: ready => set({ rpcHidReady: ready }),
|
||||
|
||||
keysDownState: undefined,
|
||||
setKeysDownState: state => set({ keysDownState: state }),
|
||||
|
||||
@@ -741,6 +729,13 @@ export interface UpdateState {
|
||||
|
||||
systemUpdateProgress: number;
|
||||
systemUpdatedAt: string | null;
|
||||
|
||||
appSignatureMissing: boolean;
|
||||
systemSignatureMissing: boolean;
|
||||
appSignatureAbsent: boolean;
|
||||
appSignatureInvalid: boolean;
|
||||
appNoPublicKey: boolean;
|
||||
signatureVerified: boolean;
|
||||
};
|
||||
setOtaState: (state: UpdateState["otaState"]) => void;
|
||||
setUpdateDialogHasBeenMinimized: (hasBeenMinimized: boolean) => void;
|
||||
@@ -789,6 +784,12 @@ export const useUpdateStore = create<UpdateState>(set => ({
|
||||
appUpdatedAt: null,
|
||||
systemUpdateProgress: 0,
|
||||
systemUpdatedAt: null,
|
||||
appSignatureMissing: false,
|
||||
systemSignatureMissing: false,
|
||||
appSignatureAbsent: false,
|
||||
appSignatureInvalid: false,
|
||||
appNoPublicKey: false,
|
||||
signatureVerified: false,
|
||||
},
|
||||
|
||||
updateDialogHasBeenMinimized: false,
|
||||
|
||||
@@ -22,6 +22,15 @@ export interface SystemVersionInfo {
|
||||
remote?: { appVersion: string; systemVersion: string };
|
||||
systemUpdateAvailable: boolean;
|
||||
appUpdateAvailable: boolean;
|
||||
appSignatureMissing?: boolean;
|
||||
systemSignatureMissing?: boolean;
|
||||
appSignatureAbsent?: boolean;
|
||||
systemSignatureAbsent?: boolean;
|
||||
appSignatureInvalid?: boolean;
|
||||
systemSignatureInvalid?: boolean;
|
||||
appNoPublicKey?: boolean;
|
||||
systemNoPublicKey?: boolean;
|
||||
signatureVerified?: boolean;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
@@ -38,6 +47,13 @@ export default function SettingsVersion() {
|
||||
const { bootStorageType } = useBootStorageType();
|
||||
const isBootFromSD = bootStorageType === "sd";
|
||||
const [isUpdateDialogOpen, setIsUpdateDialogOpen] = useState(false);
|
||||
const [signatureStatusLoading, setSignatureStatusLoading] = useState(true);
|
||||
const [signatureStatus, setSignatureStatus] = useState<{
|
||||
appSignatureAbsent: boolean;
|
||||
appSignatureInvalid: boolean;
|
||||
appNoPublicKey: boolean;
|
||||
signatureVerified: boolean;
|
||||
} | null>(null);
|
||||
const updatePanelRef = useRef<HTMLDivElement | null>(null);
|
||||
const [updateSource, setUpdateSource] = useState("github");
|
||||
const [customUpdateBaseURL, setCustomUpdateBaseURL] = useState("");
|
||||
@@ -82,6 +98,23 @@ export default function SettingsVersion() {
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setSignatureStatusLoading(true);
|
||||
send("getSelfSignatureStatus", {}, resp => {
|
||||
setSignatureStatusLoading(false);
|
||||
if ("error" in resp) return;
|
||||
const sigStatus = resp.result as {
|
||||
appSignatureAbsent: boolean;
|
||||
appSignatureInvalid: boolean;
|
||||
appNoPublicKey: boolean;
|
||||
};
|
||||
const hasSigFiles = !sigStatus.appSignatureAbsent;
|
||||
const noPublicKey = sigStatus.appNoPublicKey;
|
||||
const signatureVerified = hasSigFiles && !noPublicKey && !sigStatus.appSignatureInvalid;
|
||||
setSignatureStatus({ ...sigStatus, signatureVerified });
|
||||
});
|
||||
}, [send]);
|
||||
|
||||
const applyUpdateSource = useCallback(
|
||||
(source: string) => {
|
||||
send("setUpdateSource", { source }, resp => {
|
||||
@@ -205,6 +238,11 @@ export default function SettingsVersion() {
|
||||
}
|
||||
/>
|
||||
|
||||
<SignatureStatusCard
|
||||
signatureStatus={signatureStatus}
|
||||
signatureStatusLoading={signatureStatusLoading}
|
||||
/>
|
||||
|
||||
{!isBootFromSD && (
|
||||
<>
|
||||
<UpdateSourceSettings
|
||||
@@ -382,6 +420,7 @@ function UpdateContent({
|
||||
<SystemUpToDateState
|
||||
checkUpdate={() => setModalView("loading")}
|
||||
onClose={onClose}
|
||||
versionInfo={versionInfo}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -407,23 +446,58 @@ function LoadingState({
|
||||
|
||||
const getVersionInfo = useCallback(() => {
|
||||
return new Promise<SystemVersionInfo>((resolve, reject) => {
|
||||
send("getUpdateStatus", {}, async resp => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(`Failed to check for updates: ${resp.error}`);
|
||||
reject(new Error("Failed to check for updates"));
|
||||
} else {
|
||||
const result = resp.result as SystemVersionInfo;
|
||||
setAppVersion(result.local.appVersion);
|
||||
setSystemVersion(result.local.systemVersion);
|
||||
|
||||
if (result.error) {
|
||||
notifications.error(`Failed to check for updates: ${result.error}`);
|
||||
reject(new Error("Failed to check for updates"));
|
||||
} else {
|
||||
resolve(result);
|
||||
}
|
||||
}
|
||||
});
|
||||
Promise.all([
|
||||
new Promise<SystemVersionInfo>((res, rej) => {
|
||||
send("getUpdateStatus", {}, resp => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(`Failed to check for updates: ${resp.error}`);
|
||||
rej(new Error("Failed to check for updates"));
|
||||
} else {
|
||||
const result = resp.result as SystemVersionInfo;
|
||||
setAppVersion(result.local.appVersion);
|
||||
setSystemVersion(result.local.systemVersion);
|
||||
if (result.error) {
|
||||
notifications.error(`Failed to check for updates: ${result.error}`);
|
||||
rej(new Error("Failed to check for updates"));
|
||||
} else {
|
||||
res(result);
|
||||
}
|
||||
}
|
||||
});
|
||||
}),
|
||||
new Promise<SystemVersionInfo>((res, rej) => {
|
||||
send("getSelfSignatureStatus", {}, resp => {
|
||||
if ("error" in resp) {
|
||||
rej(new Error("Failed to get signature status"));
|
||||
} else {
|
||||
const sigStatus = resp.result as {
|
||||
appSignatureAbsent: boolean;
|
||||
appSignatureInvalid: boolean;
|
||||
appNoPublicKey: boolean;
|
||||
};
|
||||
const hasSigFiles = !sigStatus.appSignatureAbsent;
|
||||
const signatureVerified = hasSigFiles && !sigStatus.appNoPublicKey && !sigStatus.appSignatureInvalid;
|
||||
const partial: Partial<SystemVersionInfo> = {
|
||||
appSignatureAbsent: sigStatus.appSignatureAbsent,
|
||||
appSignatureInvalid: sigStatus.appSignatureInvalid,
|
||||
appNoPublicKey: sigStatus.appNoPublicKey,
|
||||
signatureVerified,
|
||||
};
|
||||
res(partial as SystemVersionInfo);
|
||||
}
|
||||
});
|
||||
}),
|
||||
])
|
||||
.then(([versionResult, sigResult]) => {
|
||||
resolve({
|
||||
...versionResult,
|
||||
appSignatureAbsent: sigResult.appSignatureAbsent,
|
||||
appSignatureInvalid: sigResult.appSignatureInvalid,
|
||||
appNoPublicKey: sigResult.appNoPublicKey,
|
||||
signatureVerified: sigResult.signatureVerified,
|
||||
});
|
||||
})
|
||||
.catch(reject);
|
||||
});
|
||||
}, [send, setAppVersion, setSystemVersion]);
|
||||
|
||||
@@ -669,11 +743,17 @@ function UpdatingDeviceState({
|
||||
function SystemUpToDateState({
|
||||
checkUpdate,
|
||||
onClose,
|
||||
versionInfo,
|
||||
}: {
|
||||
checkUpdate: () => void;
|
||||
onClose: () => void;
|
||||
versionInfo: SystemVersionInfo | null;
|
||||
}) {
|
||||
const { $at } = useReactAt();
|
||||
const hasAbsentSig = versionInfo?.appSignatureAbsent;
|
||||
const hasInvalidSig = versionInfo?.appSignatureInvalid;
|
||||
const hasNoPublicKey = versionInfo?.appNoPublicKey;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-start justify-start space-y-4 text-left">
|
||||
<div className="text-left">
|
||||
@@ -684,6 +764,50 @@ function SystemUpToDateState({
|
||||
{$at("Your system is running the latest version. No updates are currently available.")}
|
||||
</p>
|
||||
|
||||
{hasAbsentSig && (
|
||||
<div className="mt-4 rounded-md border border-yellow-500 bg-yellow-50 p-3 dark:border-yellow-600 dark:bg-yellow-900/30">
|
||||
<p className="text-sm font-medium text-yellow-800 dark:text-yellow-200">
|
||||
{$at("Missing Signature File")}
|
||||
</p>
|
||||
<p className="mt-1 text-xs text-yellow-700 dark:text-yellow-300">
|
||||
{$at("The current firmware is missing signature files. Integrity cannot be fully verified.")}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{hasInvalidSig && (
|
||||
<div className="mt-4 rounded-md border border-red-500 bg-red-50 p-3 dark:border-red-600 dark:bg-red-900/30">
|
||||
<p className="text-sm font-medium text-red-800 dark:text-red-200">
|
||||
{$at("Signature Verification Failed")}
|
||||
</p>
|
||||
<p className="mt-1 text-xs text-red-700 dark:text-red-300">
|
||||
{$at("The signature file exists but does not match the firmware. This may indicate tampering.")}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{hasNoPublicKey && (
|
||||
<div className="mt-4 rounded-md border border-yellow-500 bg-yellow-50 p-3 dark:border-yellow-600 dark:bg-yellow-900/30">
|
||||
<p className="text-sm font-medium text-yellow-800 dark:text-yellow-200">
|
||||
{$at("No Embedded Public Key")}
|
||||
</p>
|
||||
<p className="mt-1 text-xs text-yellow-700 dark:text-yellow-300">
|
||||
{$at("This build does not have an OTA public key embedded. Signature verification is unavailable.")}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{versionInfo?.signatureVerified && (
|
||||
<div className="mt-4 rounded-md border border-green-500 bg-green-50 p-3 dark:border-green-600 dark:bg-green-900/30">
|
||||
<p className="text-sm font-medium text-green-800 dark:text-green-200">
|
||||
{$at("Signature Verified")}
|
||||
</p>
|
||||
<p className="mt-1 text-xs text-green-700 dark:text-green-300">
|
||||
{$at("Firmware signature has been verified and is valid.")}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-4 flex gap-x-2">
|
||||
<AntdButton type="primary" onClick={checkUpdate}>
|
||||
{$at("Check Again")}
|
||||
@@ -825,3 +949,101 @@ function UpdateErrorState({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SignatureStatusCard({
|
||||
signatureStatus,
|
||||
signatureStatusLoading,
|
||||
}: {
|
||||
signatureStatus: {
|
||||
appSignatureAbsent: boolean;
|
||||
appSignatureInvalid: boolean;
|
||||
appNoPublicKey: boolean;
|
||||
signatureVerified: boolean;
|
||||
} | null;
|
||||
signatureStatusLoading: boolean;
|
||||
}) {
|
||||
const { $at } = useReactAt();
|
||||
if (signatureStatusLoading) {
|
||||
return (
|
||||
<div className="rounded-md border border-slate-300 bg-slate-50 p-3 dark:border-slate-600 dark:bg-slate-900/30">
|
||||
<div className="flex items-center gap-x-2">
|
||||
<LoadingSpinner className="h-4 w-4 text-[rgba(22,152,217,1)] dark:text-[rgba(45,106,229,1)]" />
|
||||
<p className="text-sm font-medium text-slate-800 dark:text-slate-200">
|
||||
{$at("Verifying signature...")}
|
||||
</p>
|
||||
</div>
|
||||
<p className="mt-1 text-xs text-slate-600 dark:text-slate-300">
|
||||
{$at("Please wait while verifying firmware signature.")}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!signatureStatus) {
|
||||
return (
|
||||
<div className="rounded-md border border-yellow-500 bg-yellow-50 p-3 dark:border-yellow-600 dark:bg-yellow-900/30">
|
||||
<p className="text-sm font-medium text-yellow-800 dark:text-yellow-200">
|
||||
{$at("Signature Status Unavailable")}
|
||||
</p>
|
||||
<p className="mt-1 text-xs text-yellow-700 dark:text-yellow-300">
|
||||
{$at("Unable to retrieve signature verification status.")}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (signatureStatus.signatureVerified) {
|
||||
return (
|
||||
<div className="rounded-md border border-green-500 bg-green-50 p-3 dark:border-green-600 dark:bg-green-900/30">
|
||||
<p className="text-sm font-medium text-green-800 dark:text-green-200">
|
||||
<CheckCircleIcon className="inline h-4 w-4 mr-1" />
|
||||
{$at("Signature Verified")}
|
||||
</p>
|
||||
<p className="mt-1 text-xs text-green-700 dark:text-green-300">
|
||||
{$at("Firmware signature has been verified and is valid.")}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (signatureStatus.appSignatureAbsent) {
|
||||
return (
|
||||
<div className="rounded-md border border-yellow-500 bg-yellow-50 p-3 dark:border-yellow-600 dark:bg-yellow-900/30">
|
||||
<p className="text-sm font-medium text-yellow-800 dark:text-yellow-200">
|
||||
{$at("Missing Signature File")}
|
||||
</p>
|
||||
<p className="mt-1 text-xs text-yellow-700 dark:text-yellow-300">
|
||||
{$at("The current firmware is missing signature files. Integrity cannot be fully verified.")}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (signatureStatus.appSignatureInvalid) {
|
||||
return (
|
||||
<div className="rounded-md border border-red-500 bg-red-50 p-3 dark:border-red-600 dark:bg-red-900/30">
|
||||
<p className="text-sm font-medium text-red-800 dark:text-red-200">
|
||||
{$at("Signature Verification Failed")}
|
||||
</p>
|
||||
<p className="mt-1 text-xs text-red-700 dark:text-red-300">
|
||||
{$at("The signature file exists but does not match the firmware. This may indicate tampering.")}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (signatureStatus.appNoPublicKey) {
|
||||
return (
|
||||
<div className="rounded-md border border-yellow-500 bg-yellow-50 p-3 dark:border-yellow-600 dark:bg-yellow-900/30">
|
||||
<p className="text-sm font-medium text-yellow-800 dark:text-yellow-200">
|
||||
{$at("No Embedded Public Key")}
|
||||
</p>
|
||||
<p className="mt-1 text-xs text-yellow-700 dark:text-yellow-300">
|
||||
{$at("This build does not have an OTA public key embedded. Signature verification is unavailable.")}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user