-
Notifications
You must be signed in to change notification settings - Fork 184
[Security] SHA-256 validation #531
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 5 commits
de137ad
6a801d0
fab98fb
eb022f3
212f992
ebeb768
be223aa
41ef1ef
753339e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -58,13 +58,55 @@ public static async Task<List<ModelFileDetails>> GetDownloadFilesFromGitHub(GitH | |
| } | ||
|
|
||
| return files.Select(f => | ||
| new ModelFileDetails() | ||
| { | ||
| string? sha256 = null; | ||
|
|
||
| if (f.Content != null && f.Encoding == "base64") | ||
| { | ||
| try | ||
| { | ||
| var decodedContent = System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(f.Content)); | ||
| sha256 = ParseLfsPointerSha256(decodedContent); | ||
| } | ||
| catch (FormatException) | ||
| { | ||
| Debug.WriteLine($"Failed to decode base64 content for {f.Path}"); | ||
| } | ||
| } | ||
|
|
||
| return new ModelFileDetails() | ||
| { | ||
| DownloadUrl = f.DownloadUrl, | ||
| Size = f.Size, | ||
| Name = (f.Path ?? string.Empty).Split(["/"], StringSplitOptions.RemoveEmptyEntries).LastOrDefault(), | ||
| Path = f.Path | ||
| }).ToList(); | ||
| Path = f.Path, | ||
| Sha256 = sha256 | ||
| }; | ||
| }).ToList(); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Parses a Git LFS pointer file content to extract the SHA256 hash. | ||
| /// </summary> | ||
| /// <param name="lfsPointerContent">The content of the LFS pointer file.</param> | ||
| /// <returns>The SHA256 hash if found, otherwise null.</returns> | ||
| private static string? ParseLfsPointerSha256(string lfsPointerContent) | ||
| { | ||
| if (string.IsNullOrEmpty(lfsPointerContent)) | ||
| { | ||
| return null; | ||
| } | ||
|
|
||
| var lines = lfsPointerContent.Split(['\n', '\r'], StringSplitOptions.RemoveEmptyEntries); | ||
| foreach (var line in lines) | ||
| { | ||
| if (line.StartsWith("oid sha256:", StringComparison.OrdinalIgnoreCase)) | ||
| { | ||
| return line.Substring("oid sha256:".Length).Trim(); | ||
| } | ||
| } | ||
|
|
||
| return null; | ||
| } | ||
|
|
||
| /// <summary> | ||
|
|
@@ -172,13 +214,26 @@ public static async Task<List<ModelFileDetails>> GetDownloadFilesFromHuggingFace | |
| } | ||
|
|
||
| return hfFiles.Where(f => f.Type != "directory").Select(f => | ||
| new ModelFileDetails() | ||
| { | ||
| string? sha256 = null; | ||
| if (f.Lfs?.Oid != null) | ||
| { | ||
| sha256 = f.Lfs.Oid; | ||
| if (sha256.StartsWith("sha256:", StringComparison.OrdinalIgnoreCase)) | ||
| { | ||
| sha256 = sha256.Substring(7); | ||
| } | ||
| } | ||
|
|
||
| return new ModelFileDetails() | ||
| { | ||
| DownloadUrl = $"https://huggingface.co/{hfUrl.Organization}/{hfUrl.Repo}/resolve/{hfUrl.Ref}/{f.Path}", | ||
| Size = f.Size, | ||
| Size = f.Lfs?.Size ?? f.Size, | ||
|
||
| Name = (f.Path ?? string.Empty).Split(["/"], StringSplitOptions.RemoveEmptyEntries).LastOrDefault(), | ||
| Path = f.Path | ||
| }).ToList(); | ||
| Path = f.Path, | ||
| Sha256 = sha256 | ||
| }; | ||
| }).ToList(); | ||
| } | ||
|
|
||
| /// <summary> | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,6 +5,7 @@ | |
| using AIDevGallery.ViewModels; | ||
| using Microsoft.UI.Xaml; | ||
| using Microsoft.UI.Xaml.Controls; | ||
| using System; | ||
| using System.Collections.ObjectModel; | ||
| using System.Linq; | ||
|
|
||
|
|
@@ -86,4 +87,60 @@ private void ClearHistory_Click(object sender, RoutedEventArgs e) | |
| } | ||
| } | ||
| } | ||
|
|
||
| private async void VerificationFailedClicked(object sender, RoutedEventArgs e) | ||
| { | ||
| if (sender is Button button && button.Tag is DownloadableModel downloadableModel) | ||
| { | ||
| var dialog = new ContentDialog | ||
| { | ||
| Title = "Integrity verification failed", | ||
| Content = new StackPanel | ||
| { | ||
| Spacing = 12, | ||
| Children = | ||
| { | ||
| new TextBlock | ||
| { | ||
| Text = "The downloaded model file(s) did not match the expected hash. This could indicate the file was corrupted during download or has been tampered with.", | ||
| TextWrapping = TextWrapping.Wrap | ||
| }, | ||
| new TextBlock | ||
| { | ||
| Text = downloadableModel.VerificationFailureMessage ?? "Unknown verification error", | ||
| TextWrapping = TextWrapping.Wrap, | ||
| Foreground = (Microsoft.UI.Xaml.Media.Brush)Application.Current.Resources["SystemFillColorCautionBrush"], | ||
| FontSize = 12 | ||
| }, | ||
| new TextBlock | ||
| { | ||
| Text = "Would you like to keep the model anyway or delete it?", | ||
| TextWrapping = TextWrapping.Wrap | ||
| } | ||
| } | ||
| }, | ||
| PrimaryButtonText = "Delete model", | ||
| SecondaryButtonText = "Keep anyway", | ||
|
||
| CloseButtonText = "Cancel", | ||
| DefaultButton = ContentDialogButton.Primary, | ||
| XamlRoot = this.XamlRoot | ||
| }; | ||
|
|
||
| var result = await dialog.ShowAsync(); | ||
|
||
|
|
||
| if (result == ContentDialogResult.Primary) | ||
| { | ||
| // Delete the model | ||
| downloadableModel.DeleteVerificationFailedModel(); | ||
| downloadProgresses.Remove(downloadableModel); | ||
| } | ||
| else if (result == ContentDialogResult.Secondary) | ||
| { | ||
| // Keep the model despite verification failure | ||
| downloadableModel.KeepVerificationFailedModel(); | ||
| } | ||
|
|
||
| // If Cancel, do nothing - leave the item in the list | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| // Copyright (c) Microsoft Corporation. All rights reserved. | ||
| // Licensed under the MIT License. | ||
|
|
||
| using Microsoft.Diagnostics.Telemetry; | ||
| using Microsoft.Diagnostics.Telemetry.Internal; | ||
| using System; | ||
| using System.Diagnostics.Tracing; | ||
|
|
||
| namespace AIDevGallery.Telemetry.Events; | ||
|
|
||
| [EventData] | ||
| internal class ModelIntegrityVerificationFailedEvent : EventBase | ||
| { | ||
| internal ModelIntegrityVerificationFailedEvent(string modelUrl, string fileName, string expectedHash, string actualHash) | ||
| { | ||
| ModelUrl = modelUrl; | ||
| FileName = fileName; | ||
| ExpectedHash = expectedHash; | ||
| ActualHash = actualHash; | ||
| EventTime = DateTime.UtcNow; | ||
| } | ||
|
|
||
| public string ModelUrl { get; private set; } | ||
| public string FileName { get; private set; } | ||
| public string ExpectedHash { get; private set; } | ||
| public string ActualHash { get; private set; } | ||
| public DateTime EventTime { get; private set; } | ||
|
|
||
| public override PartA_PrivTags PartA_PrivTags => PrivTags.ProductAndServiceUsage; | ||
|
|
||
| public override void ReplaceSensitiveStrings(Func<string?, string?> replaceSensitiveStrings) | ||
| { | ||
| } | ||
|
|
||
| public static void Log(string modelUrl, string fileName, string expectedHash, string actualHash) | ||
| { | ||
| TelemetryFactory.Get<ITelemetry>().LogError( | ||
| "ModelIntegrityVerificationFailed_Event", | ||
| LogLevel.Info, | ||
| new ModelIntegrityVerificationFailedEvent(modelUrl, fileName, expectedHash, actualHash)); | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.