| | | 1 | | using System.Collections.Immutable; |
| | | 2 | | using Microsoft.CodeAnalysis; |
| | | 3 | | using Microsoft.CodeAnalysis.Diagnostics; |
| | | 4 | | using Microsoft.CodeAnalysis.Operations; |
| | | 5 | | |
| | | 6 | | namespace AsiBackbone.Analyzers; |
| | | 7 | | |
| | | 8 | | /// <summary> |
| | | 9 | | /// Reports signing and verification request construction that omits an explicit signature input. |
| | | 10 | | /// </summary> |
| | | 11 | | [DiagnosticAnalyzer(LanguageNames.CSharp)] |
| | | 12 | | public sealed class SignatureInputRequestAnalyzer : DiagnosticAnalyzer |
| | | 13 | | { |
| | | 14 | | /// <summary> |
| | | 15 | | /// Gets the diagnostic identifier reported when a supported request omits |
| | | 16 | | /// an explicit signature input. |
| | | 17 | | /// </summary> |
| | | 18 | | public const string DiagnosticId = "ASIB004"; |
| | | 19 | | |
| | | 20 | | /// <summary> |
| | | 21 | | /// Gets the name of the property that must be set to avoid falling back to the pre-6.0 hash-only signature input. |
| | | 22 | | /// </summary> |
| | | 23 | | private const string SignatureInputPropertyName = "SignatureInput"; |
| | | 24 | | |
| | | 25 | | /// <remarks> |
| | | 26 | | /// Each of these request types falls back to the pre-6.0 hash-only signature input when <c>SignatureInput</c> is no |
| | | 27 | | /// set. The fallback is internal, so it never surfaces the <c>ASIB902</c> deprecation warning on its own. |
| | | 28 | | /// </remarks> |
| | 1 | 29 | | private static readonly string[] RequestTypeNames = |
| | 1 | 30 | | [ |
| | 1 | 31 | | "AsiBackbone.Core.Signing.SigningRequest", |
| | 1 | 32 | | "AsiBackbone.Core.Signing.SignatureVerificationRequest", |
| | 1 | 33 | | "AsiBackbone.Signing.ManagedKey.ManagedKeySignRequest", |
| | 1 | 34 | | ]; |
| | | 35 | | |
| | 1 | 36 | | private static readonly DiagnosticDescriptor Rule = new( |
| | 1 | 37 | | DiagnosticId, |
| | 1 | 38 | | "Set SignatureInput on signing and verification requests", |
| | 1 | 39 | | "'{0}' is created without setting SignatureInput, so it falls back to the pre-6.0 hash-only signature input; set |
| | 1 | 40 | | "AsiBackbone.GovernanceSafety", |
| | 1 | 41 | | DiagnosticSeverity.Warning, |
| | 1 | 42 | | isEnabledByDefault: true, |
| | 1 | 43 | | description: "A signing or verification request without an explicit SignatureInput signs or verifies the UTF-8 t |
| | | 44 | | |
| | | 45 | | /// <summary> |
| | | 46 | | /// Gets the diagnostics supported by this analyzer. |
| | | 47 | | /// </summary> |
| | 14 | 48 | | public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => [Rule]; |
| | | 49 | | |
| | | 50 | | /// <summary> |
| | | 51 | | /// Initializes the analyzer and registers operation analysis. |
| | | 52 | | /// </summary> |
| | | 53 | | /// <param name="context">The analysis context used to register analyzer actions.</param> |
| | | 54 | | public override void Initialize(AnalysisContext context) |
| | | 55 | | { |
| | 14 | 56 | | context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); |
| | 14 | 57 | | context.EnableConcurrentExecution(); |
| | 14 | 58 | | context.RegisterOperationAction(AnalyzeObjectCreation, OperationKind.ObjectCreation); |
| | 14 | 59 | | } |
| | | 60 | | |
| | | 61 | | private static void AnalyzeObjectCreation(OperationAnalysisContext context) |
| | | 62 | | { |
| | 16 | 63 | | var objectCreation = (IObjectCreationOperation)context.Operation; |
| | | 64 | | |
| | 16 | 65 | | if (objectCreation.Type is not INamedTypeSymbol requestType || !IsRequestType(requestType)) |
| | | 66 | | { |
| | 3 | 67 | | return; |
| | | 68 | | } |
| | | 69 | | |
| | 13 | 70 | | if (InitializesSignatureInput(objectCreation.Initializer)) |
| | | 71 | | { |
| | 2 | 72 | | return; |
| | | 73 | | } |
| | | 74 | | |
| | 11 | 75 | | context.ReportDiagnostic( |
| | 11 | 76 | | Diagnostic.Create( |
| | 11 | 77 | | Rule, |
| | 11 | 78 | | objectCreation.Syntax.GetLocation(), |
| | 11 | 79 | | requestType.Name)); |
| | 11 | 80 | | } |
| | | 81 | | |
| | | 82 | | private static bool IsRequestType(INamedTypeSymbol type) |
| | | 83 | | { |
| | 16 | 84 | | string typeName = type.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat); |
| | | 85 | | |
| | 71 | 86 | | foreach (string requestTypeName in RequestTypeNames) |
| | | 87 | | { |
| | 26 | 88 | | if (typeName.Equals(requestTypeName, StringComparison.Ordinal)) |
| | | 89 | | { |
| | 13 | 90 | | return true; |
| | | 91 | | } |
| | | 92 | | } |
| | | 93 | | |
| | 3 | 94 | | return false; |
| | | 95 | | } |
| | | 96 | | |
| | | 97 | | private static bool InitializesSignatureInput(IObjectOrCollectionInitializerOperation? initializer) |
| | | 98 | | { |
| | 13 | 99 | | if (initializer is null) |
| | | 100 | | { |
| | 4 | 101 | | return false; |
| | | 102 | | } |
| | | 103 | | |
| | 34 | 104 | | foreach (IOperation memberInitializer in initializer.Initializers) |
| | | 105 | | { |
| | 9 | 106 | | if (memberInitializer is ISimpleAssignmentOperation |
| | 9 | 107 | | { |
| | 9 | 108 | | Target: IPropertyReferenceOperation propertyReference |
| | 9 | 109 | | } assignment |
| | 9 | 110 | | && propertyReference.Property.Name.Equals(SignatureInputPropertyName, StringComparison.Ordinal) |
| | 9 | 111 | | && !IsStaticallyEmptySignatureInput(assignment.Value)) |
| | | 112 | | { |
| | 2 | 113 | | return true; |
| | | 114 | | } |
| | | 115 | | } |
| | | 116 | | |
| | 7 | 117 | | return false; |
| | | 118 | | } |
| | | 119 | | private static bool IsStaticallyEmptySignatureInput(IOperation operation) |
| | | 120 | | { |
| | 9 | 121 | | operation = Unwrap(operation); |
| | | 122 | | |
| | 9 | 123 | | return operation is IDefaultValueOperation || (operation is IPropertyReferenceOperation propertyReference |
| | 9 | 124 | | && propertyReference.Property.Name.Equals("Empty", StringComparison.Ordinal) |
| | 9 | 125 | | && IsMemoryLike(propertyReference.Property.ContainingType)) || (operation is IObjectCreationOperation object |
| | 9 | 126 | | && objectCreation.Arguments.Length == 0 |
| | 9 | 127 | | && IsMemoryLike(objectCreation.Type)) || IsArrayEmptyInvocation(operation) || IsStaticallyEmptyByteArray(ope |
| | | 128 | | } |
| | | 129 | | |
| | | 130 | | private static bool IsArrayEmptyInvocation(IOperation operation) |
| | | 131 | | { |
| | 4 | 132 | | return operation is IInvocationOperation invocation |
| | 4 | 133 | | && invocation.TargetMethod.Name.Equals("Empty", StringComparison.Ordinal) |
| | 4 | 134 | | && invocation.TargetMethod.ContainingType.SpecialType == SpecialType.System_Array |
| | 4 | 135 | | && invocation.TargetMethod.TypeArguments.Length == 1 |
| | 4 | 136 | | && invocation.TargetMethod.TypeArguments[0].SpecialType == SpecialType.System_Byte; |
| | | 137 | | } |
| | | 138 | | |
| | | 139 | | private static bool IsStaticallyEmptyByteArray(IOperation operation) |
| | | 140 | | { |
| | 3 | 141 | | return operation is IArrayCreationOperation arrayCreation |
| | 3 | 142 | | && arrayCreation.Type is IArrayTypeSymbol arrayType |
| | 3 | 143 | | && arrayType.ElementType.SpecialType == SpecialType.System_Byte && (arrayCreation.Initializer is { ElementVa |
| | 3 | 144 | | && arrayCreation.DimensionSizes[0].ConstantValue is { HasValue: true, Value: 0 })); |
| | | 145 | | } |
| | | 146 | | |
| | | 147 | | private static bool IsMemoryLike(ITypeSymbol? type) |
| | | 148 | | { |
| | 4 | 149 | | return type is INamedTypeSymbol namedType |
| | 4 | 150 | | && namedType.Arity == 1 |
| | 4 | 151 | | && (namedType.Name.Equals("Memory", StringComparison.Ordinal) |
| | 4 | 152 | | || namedType.Name.Equals("ReadOnlyMemory", StringComparison.Ordinal)) |
| | 4 | 153 | | && namedType.ContainingNamespace?.ToDisplayString().Equals("System", StringComparison.Ordinal) == true; |
| | | 154 | | } |
| | | 155 | | |
| | | 156 | | private static IOperation Unwrap(IOperation operation) |
| | | 157 | | { |
| | 14 | 158 | | while (operation is IConversionOperation conversion) |
| | | 159 | | { |
| | 5 | 160 | | operation = conversion.Operand; |
| | 5 | 161 | | } |
| | | 162 | | |
| | 9 | 163 | | return operation; |
| | | 164 | | } |
| | | 165 | | |
| | | 166 | | } |