Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions eng/scripts/aspire-skills-bundle.common.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,10 @@ function ConvertTo-LfUtf8Bytes {
return [System.Text.UTF8Encoding]::new($false).GetBytes($text)
}

function Get-AspireSkillsSha256Hex {
function Get-AspireSkillsSha512Hex {
param([Parameter(Mandatory = $true)][AllowEmptyCollection()][byte[]]$Bytes)

$sha = [System.Security.Cryptography.SHA256]::Create()
$sha = [System.Security.Cryptography.SHA512]::Create()
try {
return [System.BitConverter]::ToString($sha.ComputeHash($Bytes)).Replace('-', '').ToLowerInvariant()
}
Expand Down
8 changes: 4 additions & 4 deletions eng/scripts/update-aspire-skills-bundle.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ try {
Write-Host "Verifying GitHub artifact attestation for '$($asset.name)'..."
Invoke-GitHubCli attestation verify $archivePath --repo $Repository --cert-identity $certIdentity --cert-oidc-issuer 'https://token.actions.githubusercontent.com'

$hash = (Get-FileHash -Algorithm SHA256 $archivePath).Hash.ToLowerInvariant()
$hash = (Get-FileHash -Algorithm SHA512 $archivePath).Hash.ToLowerInvariant()
$targetArchivePath = Join-Path $embeddedDir $asset.name

Get-ChildItem -Path $embeddedDir -File -Force |
Expand Down Expand Up @@ -156,7 +156,7 @@ try {
Write-Host "Syncing hook script '$hookFileName' from '$Repository' at commit '$hookCommitSha'..."
$hookBytes = Get-AspireSkillsHookContent -Repository $Repository -CommitSha $hookCommitSha -FileName $hookFileName
$hookContents[$hookFileName] = $hookBytes
$hookHashes[$hookFileName] = Get-AspireSkillsSha256Hex -Bytes $hookBytes
$hookHashes[$hookFileName] = Get-AspireSkillsSha512Hex -Bytes $hookBytes
}

foreach ($hookFileName in $hookContents.Keys) {
Expand Down Expand Up @@ -185,7 +185,7 @@ try {
repository = $Repository
tag = $release.tagName
assetName = $asset.name
sha256 = $hash
sha512 = $hash
}
if ($null -ne $hookMetadata) {
$metadata['hooks'] = $hookMetadata
Expand All @@ -206,7 +206,7 @@ try {
"Agents\AspireSkills\Embedded\$($asset.name)")
Set-TextFile -Path $cliProjectPath -Content $cliProjectContent

Write-Host "Embedded Aspire skills bundle updated to '$($asset.name)' with SHA-256 '$hash'."
Write-Host "Embedded Aspire skills bundle updated to '$($asset.name)' with SHA-512 '$hash'."
}
finally {
if (Test-Path $tempDir) {
Expand Down
16 changes: 8 additions & 8 deletions eng/scripts/verify-aspire-skills-bundle.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -43,18 +43,18 @@ if ($metadata.assetName -ne [System.IO.Path]::GetFileName($metadata.assetName))
throw "Embedded Aspire skills asset name '$($metadata.assetName)' must not contain path separators."
}

if ([string]::IsNullOrWhiteSpace($metadata.sha256)) {
throw "Embedded Aspire skills metadata must specify the release asset SHA-256 hash."
if ([string]::IsNullOrWhiteSpace($metadata.sha512)) {
throw "Embedded Aspire skills metadata must specify the release asset SHA-512 hash."
}

$archivePath = Join-Path $embeddedDir $metadata.assetName
if (-not (Test-Path $archivePath)) {
throw "Embedded Aspire skills archive was not found at '$archivePath'."
}

$actualHash = (Get-FileHash -Algorithm SHA256 $archivePath).Hash.ToLowerInvariant()
if ($actualHash -ne $metadata.sha256) {
throw "Embedded bundle SHA-256 mismatch. Expected '$($metadata.sha256)', got '$actualHash'."
$actualHash = (Get-FileHash -Algorithm SHA512 $archivePath).Hash.ToLowerInvariant()
if ($actualHash -ne $metadata.sha512) {
throw "Embedded bundle SHA-512 mismatch. Expected '$($metadata.sha512)', got '$actualHash'."
}

$certIdentity = "https://github.com/$($metadata.repository)/.github/workflows/publish.yml@refs/tags/$($metadata.tag)"
Expand Down Expand Up @@ -100,12 +100,12 @@ if ($metadata.PSObject.Properties.Name -contains 'hooks') {
}

# Hash over LF-normalized bytes so .ps1 (text=auto) checked out with CRLF on Windows matches.
$embeddedHash = Get-AspireSkillsSha256Hex -Bytes (ConvertTo-LfUtf8Bytes -Bytes ([System.IO.File]::ReadAllBytes($embeddedHookPath)))
$embeddedHash = Get-AspireSkillsSha512Hex -Bytes (ConvertTo-LfUtf8Bytes -Bytes ([System.IO.File]::ReadAllBytes($embeddedHookPath)))
if ($embeddedHash -ne $recordedHash) {
throw "Embedded telemetry hook '$hookFileName' SHA-256 mismatch. Expected '$recordedHash', got '$embeddedHash'. Re-run update-aspire-skills-bundle.ps1."
throw "Embedded telemetry hook '$hookFileName' SHA-512 mismatch. Expected '$recordedHash', got '$embeddedHash'. Re-run update-aspire-skills-bundle.ps1."
}

$sourceHash = Get-AspireSkillsSha256Hex -Bytes (Get-AspireSkillsHookContent -Repository $metadata.repository -CommitSha $hooks.commitSha -FileName $hookFileName)
$sourceHash = Get-AspireSkillsSha512Hex -Bytes (Get-AspireSkillsHookContent -Repository $metadata.repository -CommitSha $hooks.commitSha -FileName $hookFileName)
if ($sourceHash -ne $recordedHash) {
throw "Telemetry hook '$hookFileName' does not match '$($metadata.repository)' at commit '$($hooks.commitSha)'. Expected '$recordedHash', got '$sourceHash'."
}
Expand Down
32 changes: 16 additions & 16 deletions extension/schemas/aspire-config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,22 +50,6 @@
"description": "Feature flags for enabling/disabling experimental or optional features. Key is feature name, value is enabled (true) or disabled (false).",
"type": "object",
"properties": {
"aspireSkillsRemoteFetchEnabled": {
"anyOf": [
{
"type": "boolean"
},
{
"type": "string",
"enum": [
"true",
"false"
]
}
],
"description": "(Preview) Allow the Aspire CLI to download the aspire-skills bundle from GitHub. When disabled (the 13.4 default), the CLI only uses the cached bundle and the embedded snapshot baked into the CLI; toggle on to opt in to the remote fetch path.",
"default": false
},
"defaultWatchEnabled": {
"anyOf": [
{
Expand Down Expand Up @@ -162,6 +146,22 @@
"description": "Enable or disable defaulting the DOTNET_NUGET_SIGNATURE_VERIFICATION environment variable for spawned processes",
"default": true
},
"polyglotIntegrationFilterEnabled": {
"anyOf": [
{
"type": "boolean"
},
{
"type": "string",
"enum": [
"true",
"false"
]
}
],
"description": "(Experimental) Restrict 'aspire add', 'aspire integration list', and 'aspire integration search' in non-C# AppHosts to integrations carrying the 'polyglot' NuGet tag. Disabled by default because no remote feed resolves the tag usefully today: Azure DevOps Artifacts feeds ignore 'tags:' query scoping, and nuget.org returns no first-party integrations for it. The filter fails closed, so enabling it against a remote feed hides every integration. Enable it only against a local package source or hive, where the tag is read from the nuspec.",
"default": false
},
"showAllTemplates": {
"anyOf": [
{
Expand Down
32 changes: 16 additions & 16 deletions extension/schemas/aspire-global-settings.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,22 +13,6 @@
"description": "Feature flags for enabling/disabling experimental or optional features. Key is feature name, value is enabled (true) or disabled (false).",
"type": "object",
"properties": {
"aspireSkillsRemoteFetchEnabled": {
"anyOf": [
{
"type": "boolean"
},
{
"type": "string",
"enum": [
"true",
"false"
]
}
],
"description": "(Preview) Allow the Aspire CLI to download the aspire-skills bundle from GitHub. When disabled (the 13.4 default), the CLI only uses the cached bundle and the embedded snapshot baked into the CLI; toggle on to opt in to the remote fetch path.",
"default": false
},
"defaultWatchEnabled": {
"anyOf": [
{
Expand Down Expand Up @@ -125,6 +109,22 @@
"description": "Enable or disable defaulting the DOTNET_NUGET_SIGNATURE_VERIFICATION environment variable for spawned processes",
"default": true
},
"polyglotIntegrationFilterEnabled": {
"anyOf": [
{
"type": "boolean"
},
{
"type": "string",
"enum": [
"true",
"false"
]
}
],
"description": "(Experimental) Restrict 'aspire add', 'aspire integration list', and 'aspire integration search' in non-C# AppHosts to integrations carrying the 'polyglot' NuGet tag. Disabled by default because no remote feed resolves the tag usefully today: Azure DevOps Artifacts feeds ignore 'tags:' query scoping, and nuget.org returns no first-party integrations for it. The filter fails closed, so enabling it against a remote feed hides every integration. Enable it only against a local package source or hive, where the tag is read from the nuspec.",
"default": false
},
"showAllTemplates": {
"anyOf": [
{
Expand Down
32 changes: 16 additions & 16 deletions extension/schemas/aspire-settings.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,22 +13,6 @@
"description": "Feature flags for enabling/disabling experimental or optional features. Key is feature name, value is enabled (true) or disabled (false).",
"type": "object",
"properties": {
"aspireSkillsRemoteFetchEnabled": {
"anyOf": [
{
"type": "boolean"
},
{
"type": "string",
"enum": [
"true",
"false"
]
}
],
"description": "(Preview) Allow the Aspire CLI to download the aspire-skills bundle from GitHub. When disabled (the 13.4 default), the CLI only uses the cached bundle and the embedded snapshot baked into the CLI; toggle on to opt in to the remote fetch path.",
"default": false
},
"defaultWatchEnabled": {
"anyOf": [
{
Expand Down Expand Up @@ -125,6 +109,22 @@
"description": "Enable or disable defaulting the DOTNET_NUGET_SIGNATURE_VERIFICATION environment variable for spawned processes",
"default": true
},
"polyglotIntegrationFilterEnabled": {
"anyOf": [
{
"type": "boolean"
},
{
"type": "string",
"enum": [
"true",
"false"
]
}
],
"description": "(Experimental) Restrict 'aspire add', 'aspire integration list', and 'aspire integration search' in non-C# AppHosts to integrations carrying the 'polyglot' NuGet tag. Disabled by default because no remote feed resolves the tag usefully today: Azure DevOps Artifacts feeds ignore 'tags:' query scoping, and nuget.org returns no first-party integrations for it. The filter fails closed, so enabling it against a remote feed hides every integration. Enable it only against a local package source or hive, where the tag is read from the nuspec.",
"default": false
},
"showAllTemplates": {
"anyOf": [
{
Expand Down
46 changes: 35 additions & 11 deletions src/Aspire.Cli/Agents/AspireSkills/AspireSkillsBundle.cs
Original file line number Diff line number Diff line change
Expand Up @@ -204,27 +204,29 @@ private static void ValidateManifest(
private static void ValidateFile(DirectoryInfo bundleDirectory, string skillName, SkillBundleFile file)
{
var relativePath = NormalizeRelativePath(file.RelativePath);
if (string.IsNullOrWhiteSpace(file.Sha256))
{
throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, "Aspire skills bundle file '{0}' in skill '{1}' does not specify a SHA-256 hash.", relativePath, skillName));
}

var fullPath = Path.Combine(bundleDirectory.FullName, SkillsDirectoryName, skillName, relativePath);
if (!File.Exists(fullPath))
{
throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, "Aspire skills bundle file '{0}' in skill '{1}' was not found.", relativePath, skillName));
}

var expectedHash = NormalizeSha256(file.Sha256);
string actualHash;
using (var stream = File.OpenRead(fullPath))
// Prefer SHA-512, which current microsoft/aspire-skills builds emit. SHA-256 remains accepted for
// bundles published before the switch — notably the attestation-verified v0.0.1 snapshot embedded in
// the CLI, whose exact bytes (and therefore its SHA-256 per-file manifest) cannot change without
// invalidating its published attestation. Once a signed SHA-512 release is re-embedded, that bundle
// carries `sha512` instead and this fallback is no longer exercised.
if (!string.IsNullOrWhiteSpace(file.Sha512))
{
actualHash = Convert.ToHexString(SHA256.HashData(stream)).ToLowerInvariant();
VerifyFileHash(fullPath, skillName, relativePath, NormalizeSha512(file.Sha512), static stream => SHA512.HashData(stream), "SHA-512");
}

if (!string.Equals(expectedHash, actualHash, StringComparison.OrdinalIgnoreCase))
else if (!string.IsNullOrWhiteSpace(file.Sha256))
{
throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, "Aspire skills bundle file '{0}' in skill '{1}' failed SHA-256 verification.", relativePath, skillName));
VerifyFileHash(fullPath, skillName, relativePath, NormalizeSha256(file.Sha256), static stream => SHA256.HashData(stream), "SHA-256");
}
else
{
throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, "Aspire skills bundle file '{0}' in skill '{1}' does not specify a SHA-512 or SHA-256 hash.", relativePath, skillName));
}

if (string.Equals(relativePath, SkillFileName, StringComparison.Ordinal))
Expand All @@ -233,6 +235,20 @@ private static void ValidateFile(DirectoryInfo bundleDirectory, string skillName
}
}

private static void VerifyFileHash(string fullPath, string skillName, string relativePath, string expectedHash, Func<Stream, byte[]> computeHash, string algorithmName)
{
string actualHash;
using (var stream = File.OpenRead(fullPath))
{
actualHash = Convert.ToHexString(computeHash(stream)).ToLowerInvariant();
}

if (!string.Equals(expectedHash, actualHash, StringComparison.OrdinalIgnoreCase))
{
throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, "Aspire skills bundle file '{0}' in skill '{1}' failed {2} verification.", relativePath, skillName, algorithmName));
}
}

private static void ValidateSkillFileFrontmatter(string skillName, string skillFilePath)
{
var content = File.ReadAllText(skillFilePath);
Expand Down Expand Up @@ -347,6 +363,14 @@ internal static string NormalizeRelativePath(string? relativePath)
return Path.Combine(segments);
}

internal static string NormalizeSha512(string sha512)
{
const string prefix = "sha512-";
return sha512.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)
? sha512[prefix.Length..]
: sha512;
}
Comment on lines +366 to +372

internal static string NormalizeSha256(string sha256)
{
const string prefix = "sha256-";
Expand Down
12 changes: 6 additions & 6 deletions src/Aspire.Cli/Agents/AspireSkills/AspireSkillsInstaller.cs
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,7 @@ private async Task<AcquisitionResult> InstallFromEmbeddedAsync(
await archiveStream.CopyToAsync(fileStream, cancellationToken).ConfigureAwait(false);
}

ValidateArchiveSha256(archivePath, metadata.Sha256!);
ValidateArchiveSha512(archivePath, metadata.Sha512!);

try
{
Expand Down Expand Up @@ -307,21 +307,21 @@ private async Task<AcquisitionResult> InstallFromEmbeddedAsync(
return AgentCommandStrings.AspireSkillsInstaller_MissingMetadataAssetName;
}

if (string.IsNullOrWhiteSpace(metadata.Sha256))
if (string.IsNullOrWhiteSpace(metadata.Sha512))
{
return AgentCommandStrings.AspireSkillsInstaller_MissingMetadataSha256;
return AgentCommandStrings.AspireSkillsInstaller_MissingMetadataSha512;
}

return null;
}

private static void ValidateArchiveSha256(string archivePath, string expectedSha256)
private static void ValidateArchiveSha512(string archivePath, string expectedSha512)
{
var expectedHash = AspireSkillsBundle.NormalizeSha256(expectedSha256);
var expectedHash = AspireSkillsBundle.NormalizeSha512(expectedSha512);
string actualHash;
using (var stream = File.OpenRead(archivePath))
{
actualHash = Convert.ToHexString(SHA256.HashData(stream)).ToLowerInvariant();
actualHash = Convert.ToHexString(SHA512.HashData(stream)).ToLowerInvariant();
}

if (!string.Equals(expectedHash, actualHash, StringComparison.OrdinalIgnoreCase))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@
"repository": "microsoft/aspire-skills",
"tag": "v0.0.1",
"assetName": "aspire-skills-v0.0.1.tgz",
"sha256": "8a8022a250818c730c78a5054f485e0a09ec80ac60da63910cf5091684ceec9a"
"sha512": "d968e7c9268d92c5964490ee5f96182c06a1d48879fe3f56e496544c2ec172596307a82cb4d8d1ee5f680db33570c3f12aae8768f186a5deac973d2ca06ca8bf"
}
12 changes: 11 additions & 1 deletion src/Aspire.Cli/Agents/AspireSkills/SkillBundleManifest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,15 @@ internal sealed class SkillBundleFile
{
public string? RelativePath { get; init; }

// Lowercase hex SHA-512 of the file contents (preferred), read from `skill-manifest.json` inside the
// bundle archive (an optional `sha512-` SRI-style prefix is tolerated). Emitted per-file by current
// microsoft/aspire-skills' build-aspire-bundles.mjs and verified by AspireSkillsBundle.ValidateFile.
public string? Sha512 { get; init; }

// Lowercase hex SHA-256 of the file contents, accepted only for bundles published before the SHA-512
// switch — notably the attestation-verified v0.0.1 snapshot currently embedded in the CLI, whose bytes
// cannot be re-hashed without breaking their published attestation. When both are present SHA-512 wins;
// an optional `sha256-` SRI-style prefix is tolerated. New/remote bundles emit `Sha512` and this is null.
Comment on lines +55 to +62
public string? Sha256 { get; init; }
}

Expand All @@ -67,7 +76,8 @@ internal sealed class EmbeddedAspireSkillsBundleMetadata

public string? AssetName { get; init; }

public string? Sha256 { get; init; }
// Lowercase hex SHA-512 of the embedded `.tgz` archive; verified by AspireSkillsInstaller before extraction.
public string? Sha512 { get; init; }
}

/// <summary>
Expand Down
2 changes: 2 additions & 0 deletions src/Aspire.Cli/Commands/ConfigCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,7 @@ private async Task<int> ExecuteAsync(bool showAll, CancellationToken cancellatio
.ToHashSet(StringComparer.Ordinal);

var unconfiguredFeatures = KnownFeatures.GetAllFeatureMetadata()
.Where(f => !f.Hidden)
.Where(f => !allConfiguredFeatures.Contains(f.Name))
.ToList();

Expand Down Expand Up @@ -464,6 +465,7 @@ private Task<int> ExecuteAsync(bool useJson)
var localPath = ConfigurationService.GetSettingsFilePath(isGlobal: false);
var globalPath = ConfigurationService.GetSettingsFilePath(isGlobal: true);
var availableFeatures = KnownFeatures.GetAllFeatureMetadata()
.Where(m => !m.Hidden)
.Select(m => new FeatureInfo(m.Name, m.Description, m.DefaultValue))
.ToList();
var localSchema = SettingsSchemaBuilder.BuildSchema(excludeLocalOnly: false);
Expand Down
Loading
Loading