| File: EnsureDotNetWatchBrowserToolsKey.cs | Web Access |
| Project: Microsoft.NET.Sdk.StaticWebAssets.Tasks.csproj (Microsoft.NET.Sdk.StaticWebAssets.Tasks) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. #nullable disable using Microsoft.AspNetCore.StaticWebAssets.Tasks.Utils; using Microsoft.Build.Framework; namespace Microsoft.AspNetCore.StaticWebAssets.Tasks; /// <summary> /// Ensures that the project has a <c>dotnet watch</c> browser tools key pair in its intermediate /// output and reports the public half. /// /// The browser authenticates the <c>dotnet watch</c> provider with a key that the application pins /// at build time, so the key has to be produced by the build rather than handed to it: a provider /// that supplied its own key would be authenticating itself. The build therefore owns the key pair, /// writes the two halves to separate files, and only the public half is ever pinned into an /// application asset. /// /// The task is incremental. An existing pair is reused whenever both documents parse, have the /// current version and algorithm, and describe the same key, which keeps the generated configuration /// module byte identical across rebuilds and keeps downstream static web asset incrementality /// intact. Anything else - a missing file, a truncated file, a document from an older SDK, or two /// files that do not belong to each other - regenerates both halves. /// /// The private half is never logged, never returned as an output, and never becomes a static web /// asset. Both files are reported through <c>FileWrites</c> by the caller so that <c>Clean</c> /// removes them. /// </summary> public class EnsureDotNetWatchBrowserToolsKey : Task { /// <summary> /// Path of the JSON document that holds the base64 X.509 SubjectPublicKeyInfo. /// </summary> [Required] public string PublicKeyPath { get; set; } /// <summary> /// Path of the JSON document that holds the private key components. Consumed only by /// <c>dotnet watch</c>. /// </summary> [Required] public string PrivateKeyPath { get; set; } /// <summary> /// Base64 X.509 SubjectPublicKeyInfo of the key pair. Safe to pin into build output. /// </summary> [Output] public string PublicKey { get; set; } /// <summary> /// True when a new key pair had to be created. Only used for diagnostics and tests. /// </summary> [Output] public bool Regenerated { get; set; } public override bool Execute() { try { if (TryReuseExistingKeyPair(out var existingPublicKey)) { PublicKey = existingPublicKey; Regenerated = false; Log.LogMessage(MessageImportance.Low, "Reusing the existing dotnet-watch browser tools key pair."); return true; } var (publicKey, parameters) = BrowserToolsKeyPair.Create(); var directory = Path.GetDirectoryName(PrivateKeyPath); if (!string.IsNullOrEmpty(directory)) { Directory.CreateDirectory(directory); } directory = Path.GetDirectoryName(PublicKeyPath); if (!string.IsNullOrEmpty(directory)) { Directory.CreateDirectory(directory); } // The private half is written first. If the process is interrupted between the two // writes the public document is missing, which the reuse check treats as invalid, so an // interrupted run can never leave a public key that no private key matches. File.WriteAllBytes(PrivateKeyPath, BrowserToolsKeyPair.CreatePrivateKeyDocument(publicKey, parameters)); File.WriteAllBytes(PublicKeyPath, BrowserToolsKeyPair.CreatePublicKeyDocument(publicKey)); PublicKey = publicKey; Regenerated = true; Log.LogMessage(MessageImportance.Low, "Generated a new dotnet-watch browser tools key pair."); return true; } catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) { // The message of an IO exception contains paths only, never key material. Log.LogError("Unable to create the dotnet-watch browser tools key pair: {0}", ex.Message); return false; } } private bool TryReuseExistingKeyPair(out string publicKey) { publicKey = null; var publicDocument = BrowserToolsKeyPair.TryReadPublicKeyFile(PublicKeyPath); if (!publicDocument.IsValid) { Log.LogMessage(MessageImportance.Low, "Regenerating the dotnet-watch browser tools key pair because the public key document is not usable: {0}.", publicDocument.Reason); return false; } var privateDocument = BrowserToolsKeyPair.TryReadPrivateKeyFile(PrivateKeyPath); if (!privateDocument.IsValid) { Log.LogMessage(MessageImportance.Low, "Regenerating the dotnet-watch browser tools key pair because the private key document is not usable: {0}.", privateDocument.Reason); return false; } if (!string.Equals(publicDocument.PublicKey, privateDocument.PublicKey, StringComparison.Ordinal)) { Log.LogMessage(MessageImportance.Low, "Regenerating the dotnet-watch browser tools key pair because the two documents describe different keys."); return false; } // The recorded public key is not trusted on its own: it is re-derived from the private // components so that a hand edited or partially written pair cannot pin a key that the // provider is unable to use. if (!BrowserToolsKeyPair.TryDerivePublicKey(privateDocument.Parameters, out var derivedPublicKey) || !string.Equals(derivedPublicKey, publicDocument.PublicKey, StringComparison.Ordinal)) { Log.LogMessage(MessageImportance.Low, "Regenerating the dotnet-watch browser tools key pair because the private key does not match the public key."); return false; } publicKey = publicDocument.PublicKey; return true; } }