diff --git a/.github/workflows/interoperability-reference-guard.yml b/.github/workflows/interoperability-reference-guard.yml index 09e296d31..7d509906e 100644 --- a/.github/workflows/interoperability-reference-guard.yml +++ b/.github/workflows/interoperability-reference-guard.yml @@ -88,6 +88,8 @@ jobs: . .\ARSAS\scripts\test-m217k-typed-status-trial.ps1 $coreRoutingTrialValid = Test-P109FCoreRoutingTrial -Lock $lock $m217KTrialValid = Test-M217KTypedStatusTrial -Lock $lock + . .\ARSAS\scripts\test-p0-a-identity-trial.ps1 + $p0AIdentityTrialValid = Test-P0AIdentityTrial -Lock $lock $trial = $lock.smartInteroperabilityCodeTrial $serverAt = $lock.sclServerAtTrial $originalTrial = $lock.commit -eq $trial.exactCommit -and @@ -135,7 +137,7 @@ jobs: if ($trial.status -ne 'code-verified-not-physical' -or [int]$trial.issue -ne 144 -or [int]$trial.integrationPullRequest -ne 148 -or - -not ($originalTrial -or $serverAtTrial -or $datasetTrial -or $p10Trial -or $coreRoutingTrialValid -or $m217KTrialValid) -or + -not ($originalTrial -or $serverAtTrial -or $datasetTrial -or $p10Trial -or $coreRoutingTrialValid -or $m217KTrialValid -or $p0AIdentityTrialValid) -or -not [bool]$trial.fullDiscoveryFallbackForbidden -or -not [bool]$trial.vendorBranchingForbidden -or -not [bool]$trial.physicalPromotionRequired) { diff --git a/.github/workflows/scl-interoperability-r7.yml b/.github/workflows/scl-interoperability-r7.yml index 09ce6eb2a..27a4151dc 100644 --- a/.github/workflows/scl-interoperability-r7.yml +++ b/.github/workflows/scl-interoperability-r7.yml @@ -85,6 +85,8 @@ jobs: . .\ARSAS\scripts\test-m217k-typed-status-trial.ps1 $coreRoutingTrialValid = Test-P109FCoreRoutingTrial -Lock $lock $m217KTrialValid = Test-M217KTypedStatusTrial -Lock $lock + . .\ARSAS\scripts\test-p0-a-identity-trial.ps1 + $p0AIdentityTrialValid = Test-P0AIdentityTrial -Lock $lock $trial = $lock.smartInteroperabilityCodeTrial if ($null -ne $trial -and $lock.commit -eq $trial.exactCommit) { if ([int]$lock.sourcePullRequest -ne 148 -or @@ -150,7 +152,7 @@ jobs: } Write-Host 'P10 trusted-SCL identity trial accepted for code CI ONLY; R7/R10 physical gate unchanged.' } - elseif ($m217KTrialValid) { + elseif ($m217KTrialValid -or $p0AIdentityTrialValid) { if ($env:GITHUB_BASE_REF -eq 'main' -or $env:GITHUB_REF_NAME -eq 'main') { throw 'M2.17-K is not physically accepted and cannot run as a mainline promotion.' } diff --git a/.github/workflows/smart-discovery-mainline-readiness.yml b/.github/workflows/smart-discovery-mainline-readiness.yml index 44a927237..e26c26f2e 100644 --- a/.github/workflows/smart-discovery-mainline-readiness.yml +++ b/.github/workflows/smart-discovery-mainline-readiness.yml @@ -113,6 +113,8 @@ jobs: . .\scripts\test-m217k-typed-status-trial.ps1 $coreRoutingTrialValid = Test-P109FCoreRoutingTrial -Lock $lock $m217KTrialValid = Test-M217KTypedStatusTrial -Lock $lock + . .\scripts\test-p0-a-identity-trial.ps1 + $p0AIdentityTrialValid = Test-P0AIdentityTrial -Lock $lock $trial = $lock.smartInteroperabilityCodeTrial $serverAt = $lock.sclServerAtTrial $closure = $lock.datasetFirstDiscoveryTrial @@ -164,7 +166,7 @@ jobs: $lock.commit -eq $p10.exactCommit -and [int]$lock.sourcePullRequest -eq 161 - if ($originalTrial -or $serverAtTrial -or $datasetTrial -or $p10Trial -or $coreRoutingTrialValid -or $m217KTrialValid) { + if ($originalTrial -or $serverAtTrial -or $datasetTrial -or $p10Trial -or $coreRoutingTrialValid -or $m217KTrialValid -or $p0AIdentityTrialValid) { if (-not $historicalTrialValid -or $lock.ref -ne 'main') { throw 'Stacked trial identity or ancestor authority is inconsistent.' diff --git a/Services/TrustedSclIdentityAuthority.cs b/Services/TrustedSclIdentityAuthority.cs index 2e894f5c0..a5795c14a 100644 --- a/Services/TrustedSclIdentityAuthority.cs +++ b/Services/TrustedSclIdentityAuthority.cs @@ -29,7 +29,8 @@ public static class TrustedSclIdentityAuthority source, liveModel.LogicalDevices.Select(ld => ld.MmsDomain), device.IpAddress, - device.SclIedName); + device.SclIedName, + device.SclAccessPointName); } /// @@ -87,7 +88,8 @@ public static class TrustedSclIdentityAuthority continue; var identity = TrustedSclIedIdentityMatcher.TryMatch( - document, domains, device.IpAddress, candidate.IedName); + document, domains, device.IpAddress, candidate.IedName, + candidate.AccessPointName); if (identity is null || identity.Source != "TrustedSclExactDomainMatch" || identity.Confidence != LiveIedDiscoveryConfidenceLevel.High) diff --git a/engines/ARIEC61850.lock.json b/engines/ARIEC61850.lock.json index 8cc894215..0cae6ece8 100644 --- a/engines/ARIEC61850.lock.json +++ b/engines/ARIEC61850.lock.json @@ -2,13 +2,13 @@ "schemaVersion": 1, "repository": "masarray/ARIEC61850", "ref": "main", - "commit": "51c2eefd61d6bcbe1116fda6b781b74bb9cf06da", - "sourcePullRequest": 173, - "purpose": "M2.17-K isolated C264 acceptance trial: ARIEC typed ST enumerated Mod/Beh/Health/PhyHealth Report FCD projection with exact ST/quality/timestamp checks, preserving P10.9V control and fail-closed unknowns. ARIEC #173 Windows engine CI passed; integrated consumer CI/physical C264 replay pending. Do not promote or merge into main without acceptance.", + "commit": "ccf31f60095ea65f9095514f0fa0102060962049", + "sourcePullRequest": 177, + "purpose": "P0-A trial of generic exact-prefix identity, provisional MMS-only confidence, case-sensitive LD domains and endpoint/AP-bound trusted SCL reconciliation. Engine exact-SHA CI passed; consumer exact-SHA Windows CI and physical qualification required; no mainline promotion.", "previousTrialPin": { "commit": "4467124775d8d9d76f3db194f9fbfd97144767a8", "sourcePullRequest": 134, - "purpose": "Previous P0-5b/P0-5c field-capture engine pin retained as the physical R5 baseline before canonical communication evidence, logical RCB SCL projection, and round-trip association validation were added." + "purpose": "Historical P0-5b/P0-5c field-capture engine baseline required by immutable golden evidence and smart-discovery capture gates." }, "priorGoldenWirePin": { "commit": "0023ef9a4373855497464ed3979e359c4041c95d", @@ -270,5 +270,19 @@ "physicalQualificationRequired": true, "mainlinePromotionForbidden": true, "cyclicMmsProcessPollingForbidden": true + }, + "p0AIdentityTrial": { + "status": "code-verified-engine-consumer-trial-not-physical", + "enginePullRequest": 177, + "consumerPullRequest": 625, + "baseEngineCommit": "51c2eefd61d6bcbe1116fda6b781b74bb9cf06da", + "exactCommit": "ccf31f60095ea65f9095514f0fa0102060962049", + "engineCiRun": 39228439974, + "fullDomainSetRequired": true, + "sclSha256Required": true, + "endpointAndAccessPointRequired": true, + "provisionalHeuristicOnly": true, + "physicalQualificationRequired": true, + "mainlinePromotionForbidden": true } } diff --git a/scripts/resolve-ariec-lock.ps1 b/scripts/resolve-ariec-lock.ps1 index 2856f8fbf..c70266497 100644 --- a/scripts/resolve-ariec-lock.ps1 +++ b/scripts/resolve-ariec-lock.ps1 @@ -121,13 +121,16 @@ if ($null -ne $lock.sclAssociationInteroperability) { . (Join-Path $PSScriptRoot 'test-m217k-typed-status-trial.ps1') $coreRoutingTrialValid = Test-P109FCoreRoutingTrial -Lock $lock $m217KTrialValid = Test-M217KTypedStatusTrial -Lock $lock + . (Join-Path $PSScriptRoot 'test-p0-a-identity-trial.ps1') + $p0AIdentityTrialValid = Test-P0AIdentityTrial -Lock $lock $acceptedTrial = ( ($trialValid -and [string]$lock.commit -ceq [string]$trial.exactCommit -and [int]$lock.sourcePullRequest -eq 148) -or ($serverAtValid -and [string]$lock.commit -ceq [string]$serverAt.exactCommit -and [int]$lock.sourcePullRequest -eq 153) -or ($closureValid -and [string]$lock.commit -ceq [string]$closure.exactCommit -and [int]$lock.sourcePullRequest -eq 157) -or ($p10Valid -and [string]$lock.commit -ceq [string]$p10.exactCommit -and [int]$lock.sourcePullRequest -eq 161) -or ($p10Valid -and $coreRoutingTrialValid) -or - $m217KTrialValid + $m217KTrialValid -or + $p0AIdentityTrialValid ) if (-not $historicalAnchorValid -or -not $acceptedTrial) { throw 'SCL association physical-tested engine head differs from integration lock.' diff --git a/scripts/test-p0-a-identity-trial.ps1 b/scripts/test-p0-a-identity-trial.ps1 new file mode 100644 index 000000000..ad509be37 --- /dev/null +++ b/scripts/test-p0-a-identity-trial.ps1 @@ -0,0 +1,34 @@ +# P0-A identity is a code-only descendant of the immutable M2.17-K engine. +# This verifier deliberately never changes historical physical acceptance. +function Test-P0AIdentityTrial { + param([Parameter(Mandatory = $true)]$Lock) + + $trial = $Lock.p0AIdentityTrial + if ($null -eq $trial) { return $false } + + . (Join-Path $PSScriptRoot 'test-m217k-typed-status-trial.ps1') + $parent = $Lock | ConvertTo-Json -Depth 100 | ConvertFrom-Json + $parent.commit = '51c2eefd61d6bcbe1116fda6b781b74bb9cf06da' + $parent.sourcePullRequest = 173 + $parent.purpose = 'M2.17-K isolated C264 acceptance trial' + if (-not (Test-M217KTypedStatusTrial -Lock $parent)) { return $false } + + return ( + [string]$Lock.repository -ceq 'masarray/ARIEC61850' -and + [string]$Lock.ref -ceq 'main' -and + [string]$Lock.commit -ceq 'ccf31f60095ea65f9095514f0fa0102060962049' -and + [int]$Lock.sourcePullRequest -eq 177 -and + [string]$trial.status -ceq 'code-verified-engine-consumer-trial-not-physical' -and + [int]$trial.enginePullRequest -eq 177 -and + [int]$trial.consumerPullRequest -eq 625 -and + [string]$trial.baseEngineCommit -ceq '51c2eefd61d6bcbe1116fda6b781b74bb9cf06da' -and + [string]$trial.exactCommit -ceq $Lock.commit -and + [long]$trial.engineCiRun -eq 39228439974 -and + [bool]$trial.fullDomainSetRequired -and + [bool]$trial.sclSha256Required -and + [bool]$trial.endpointAndAccessPointRequired -and + [bool]$trial.provisionalHeuristicOnly -and + [bool]$trial.physicalQualificationRequired -and + [bool]$trial.mainlinePromotionForbidden + ) +} diff --git a/tests/ARSAS.Tests/CanonicalLiveSclExportRegressionTests.cs b/tests/ARSAS.Tests/CanonicalLiveSclExportRegressionTests.cs index e3351a288..2fe8376f4 100644 --- a/tests/ARSAS.Tests/CanonicalLiveSclExportRegressionTests.cs +++ b/tests/ARSAS.Tests/CanonicalLiveSclExportRegressionTests.cs @@ -474,9 +474,12 @@ public void EngineLock_PreservesPhysicalSclRepairBaselineAcrossCodeVerifiedInter typedStatusTrial.GetProperty("previousConsumerPin").GetString()); Assert.Equal("51c2eefd61d6bcbe1116fda6b781b74bb9cf06da", typedStatusTrial.GetProperty("exactCommit").GetString()); + var identityTrial = root.GetProperty("p0AIdentityTrial"); Assert.Equal(typedStatusTrial.GetProperty("exactCommit").GetString(), + identityTrial.GetProperty("baseEngineCommit").GetString()); + Assert.Equal(identityTrial.GetProperty("exactCommit").GetString(), root.GetProperty("commit").GetString()); - Assert.Equal(173, root.GetProperty("sourcePullRequest").GetInt32()); + Assert.Equal(177, root.GetProperty("sourcePullRequest").GetInt32()); Assert.Equal(618, typedStatusTrial.GetProperty("consumerPullRequest").GetInt32()); Assert.True(typedStatusTrial.GetProperty("strictFcAndTupleEvidenceRequired").GetBoolean()); Assert.True(typedStatusTrial.GetProperty("physicalQualificationRequired").GetBoolean()); @@ -518,9 +521,9 @@ public void EngineLock_PreservesPhysicalSclRepairBaselineAcrossCodeVerifiedInter Assert.True(zivTrial.GetProperty("mainlinePromotionForbidden").GetBoolean()); Assert.True(zivTrial.GetProperty("fullDiscoveryFallbackForbidden").GetBoolean()); Assert.True(zivTrial.GetProperty("cyclicMmsProcessPollingForbidden").GetBoolean()); - // The current C264 report trial is PR #173, while the previous - // P10.9V control authority remains the unchanged PR #169. - Assert.Equal(173, root.GetProperty("sourcePullRequest").GetInt32()); + // Historical C264 remains PR #173; current P0-A identity trial is #177. + Assert.Equal(173, typedStatusTrial.GetProperty("sourcePullRequest").GetInt32()); + Assert.Equal(177, root.GetProperty("sourcePullRequest").GetInt32()); Assert.Equal(169, typedBscTrial.GetProperty("enginePullRequest").GetInt32()); Assert.True(genericTrial.GetProperty("typedSemanticGateRequired").GetBoolean()); Assert.True(genericTrial.GetProperty("associationScopedInventoryRequired").GetBoolean()); diff --git a/tests/ARSAS.Tests/M217KC264TypedStatusReportIntegrationTests.cs b/tests/ARSAS.Tests/M217KC264TypedStatusReportIntegrationTests.cs index 431c34b23..457489d0f 100644 --- a/tests/ARSAS.Tests/M217KC264TypedStatusReportIntegrationTests.cs +++ b/tests/ARSAS.Tests/M217KC264TypedStatusReportIntegrationTests.cs @@ -68,9 +68,11 @@ public void TestProductUsesExplicitlyApprovedEngineCommitNotPreviouslyPinnedDll( using var lockDocument = JsonDocument.Parse(File.ReadAllText( Path.Combine(root!.FullName, "engines", "ARIEC61850.lock.json"))); var lockRoot = lockDocument.RootElement; - Assert.Equal(ExpectedEngineSha, + var identityTrial = lockRoot.GetProperty("p0AIdentityTrial"); + Assert.Equal(ExpectedEngineSha, identityTrial.GetProperty("baseEngineCommit").GetString()); + Assert.Equal(identityTrial.GetProperty("exactCommit").GetString(), lockRoot.GetProperty("commit").GetString()); - Assert.Equal(173, lockRoot.GetProperty("sourcePullRequest").GetInt32()); + Assert.Equal(177, lockRoot.GetProperty("sourcePullRequest").GetInt32()); var trial = lockRoot.GetProperty("m217KTypedStatusReportTrial"); Assert.Equal("036e2c00d83a0815b6160147b14ea05975057965", trial.GetProperty("previousConsumerPin").GetString()); diff --git a/tests/ARSAS.Tests/P10MainlinePolicyExecutionTests.cs b/tests/ARSAS.Tests/P10MainlinePolicyExecutionTests.cs index e4f1db0b9..740361e0d 100644 --- a/tests/ARSAS.Tests/P10MainlinePolicyExecutionTests.cs +++ b/tests/ARSAS.Tests/P10MainlinePolicyExecutionTests.cs @@ -116,9 +116,22 @@ public void Exact_CI_Gate_Fails_Closed_Without_False_Physical_Promotion( // must execute the same immutable-trial gate, not a missing script. File.Copy(Path.Combine(root, "scripts", "test-m217k-typed-status-trial.ps1"), Path.Combine(folder, "scripts", "test-m217k-typed-status-trial.ps1")); + File.Copy(Path.Combine(root, "scripts", "test-p0-a-identity-trial.ps1"), + Path.Combine(folder, "scripts", "test-p0-a-identity-trial.ps1")); var lockDoc = JsonNode.Parse(File.ReadAllText(Path.Combine(root, "engines", "ARIEC61850.lock.json")))!.AsObject(); + // Historical R10 gate tests its own already-qualified M2.17-K + // baseline. The P0-A child is a distinct non-physical trial, not + // retroactive evidence that R10's historical pin changed. + if (scenario == "p10-stacked-valid" || + scenario.StartsWith("m217k-", StringComparison.Ordinal)) + { + lockDoc["commit"] = "51c2eefd61d6bcbe1116fda6b781b74bb9cf06da"; + lockDoc["sourcePullRequest"] = 173; + lockDoc["purpose"] = "M2.17-K isolated C264 acceptance trial"; + } + string baseRef = "fix/512-goose-event-header-alignment"; string sourceRef = "512/merge"; switch (scenario) diff --git a/tests/ARSAS.Tests/SmartInteroperabilityTrialGuardRegressionTests.cs b/tests/ARSAS.Tests/SmartInteroperabilityTrialGuardRegressionTests.cs index 0225ca225..ea739723b 100644 --- a/tests/ARSAS.Tests/SmartInteroperabilityTrialGuardRegressionTests.cs +++ b/tests/ARSAS.Tests/SmartInteroperabilityTrialGuardRegressionTests.cs @@ -118,9 +118,14 @@ public void TrialLock_PreservesHistoricalPhysicalAuthorityAndExplicitlyRequiresP typedStatusTrial.GetProperty("previousConsumerPin").GetString()); Assert.Equal("51c2eefd61d6bcbe1116fda6b781b74bb9cf06da", typedStatusTrial.GetProperty("exactCommit").GetString()); + var identityTrial = root.GetProperty("p0AIdentityTrial"); Assert.Equal(typedStatusTrial.GetProperty("exactCommit").GetString(), + identityTrial.GetProperty("baseEngineCommit").GetString()); + Assert.Equal(identityTrial.GetProperty("exactCommit").GetString(), root.GetProperty("commit").GetString()); - Assert.Equal(173, root.GetProperty("sourcePullRequest").GetInt32()); + Assert.Equal(177, root.GetProperty("sourcePullRequest").GetInt32()); + Assert.True(identityTrial.GetProperty("physicalQualificationRequired").GetBoolean()); + Assert.True(identityTrial.GetProperty("mainlinePromotionForbidden").GetBoolean()); Assert.Equal(618, typedStatusTrial.GetProperty("consumerPullRequest").GetInt32()); Assert.True(typedStatusTrial.GetProperty("strictFcAndTupleEvidenceRequired").GetBoolean()); Assert.True(typedStatusTrial.GetProperty("physicalQualificationRequired").GetBoolean()); @@ -166,9 +171,9 @@ public void TrialLock_PreservesHistoricalPhysicalAuthorityAndExplicitlyRequiresP Assert.True(zivTrial.GetProperty("mainlinePromotionForbidden").GetBoolean()); Assert.True(zivTrial.GetProperty("fullDiscoveryFallbackForbidden").GetBoolean()); Assert.True(zivTrial.GetProperty("cyclicMmsProcessPollingForbidden").GetBoolean()); - // The current C264 report trial is PR #173, while the previous - // P10.9V control authority remains the unchanged PR #169. - Assert.Equal(173, root.GetProperty("sourcePullRequest").GetInt32()); + // Historical C264 remains PR #173; current P0-A identity trial is #177. + Assert.Equal(173, typedStatusTrial.GetProperty("sourcePullRequest").GetInt32()); + Assert.Equal(177, root.GetProperty("sourcePullRequest").GetInt32()); Assert.Equal(169, typedBscTrial.GetProperty("enginePullRequest").GetInt32()); Assert.Equal(540, genericTrial.GetProperty("issue").GetInt32()); Assert.Equal(38231327892L, genericTrial.GetProperty("engineCiRun").GetInt64()); diff --git a/tests/ARSAS.Tests/TrustedSclIdentityConsumerTests.cs b/tests/ARSAS.Tests/TrustedSclIdentityConsumerTests.cs index f2476d095..9a6b99c91 100644 --- a/tests/ARSAS.Tests/TrustedSclIdentityConsumerTests.cs +++ b/tests/ARSAS.Tests/TrustedSclIdentityConsumerTests.cs @@ -23,6 +23,40 @@ public async Task GE_F650_SHA_Pinned_Engineering_Source_Promotes_IED_And_LD_With Assert.Equal("F650",Assert.Single(projected.LogicalDevices).Instance); } + + [Fact] + public async Task Five_Ld_SIEBCU_Identity_Is_Verified_Only_By_Matching_Pinned_Scl_Endpoint_And_Ap() + { + using var temp=TempScl.Create("SIEBCU","CTRL","192.0.2.20"); + var path=temp.Device.SclSourcePath; + var xml=File.ReadAllText(path); + xml=xml.Replace("", """ + + """, StringComparison.Ordinal); + File.WriteAllText(path,xml); + temp.Device.SclSourceSha256=Convert.ToHexString(SHA256.HashData(File.ReadAllBytes(path))); + var domains=new[] {"SIEBCUCTRL","SIEBCUMEAS","SIEBCUPROT","SIEBCUUD1","SIEBCUUD2"}; + var model=new LiveIedModelDiscoveryDocument + { + Host="192.0.2.20", + LogicalDevices=domains.Select(x=>new LiveIedLogicalDeviceModel{MmsDomain=x,Inst=x}).ToArray() + }; + var verified=await TrustedSclIdentityAuthority.TryMatchAsync(temp.Device,model); + Assert.NotNull(verified); + Assert.Equal("SIEBCU",verified.IedName); + Assert.Equal(LiveIedDiscoveryConfidenceLevel.High,verified.Confidence); + Assert.Equal(5,verified.LogicalDeviceAliases.Count); + var projected=TrustedSclIdentityAuthority.ToConsumerIdentity(verified,domains); + Assert.Equal("SIEBCU",projected.IedName); + Assert.Equal("UD2",projected.LogicalDevices.Single(x=>x.Domain=="SIEBCUUD2").Instance); + + temp.Device.SclAccessPointName="WRONG_AP"; + Assert.Null(await TrustedSclIdentityAuthority.TryMatchAsync(temp.Device,model)); + temp.Device.SclAccessPointName="S1"; + temp.Device.IpAddress="192.0.2.21"; + Assert.Null(await TrustedSclIdentityAuthority.TryMatchAsync(temp.Device,model)); + } + [Fact] public async Task Changed_SCL_SHA_Must_Fail_Closed_And_Never_Rename() {