From 8adbecaf892588406ac809110ffb11ccadf3506a Mon Sep 17 00:00:00 2001 From: Mark Xue Date: Tue, 18 Aug 2026 15:29:27 -0700 Subject: [PATCH] Fix the build after DIDDocument's optional relaxation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #58 made `verificationMethod` and `publicKeyMultibase` optional to match the canonical DID schema; #54 added RepoSigningKey, which unwraps both. Each PR was green on its own base and they merge cleanly as text, so main landed broken — `swift build` fails at RepoSigningKey.swift:53. Both absent cases now refuse with noAtprotoSigningKey, the same answer an empty method list already gave: a document that publishes no key material for this DID proves nothing about its repo, so the only sound response is to refuse. Adds a regression test for each, since these branches are reachable only from hand-written JSON and would otherwise be settled by whoever next touches the unwrap. The multibase case is mutation-verified. --- .changeset/brave-hounds-unwrap.md | 5 +++ .../Signing/RepoSigningKey.swift | 13 +++++- .../RepoProofVerifierTests.swift | 44 +++++++++++++++++++ 3 files changed, 60 insertions(+), 2 deletions(-) create mode 100644 .changeset/brave-hounds-unwrap.md diff --git a/.changeset/brave-hounds-unwrap.md b/.changeset/brave-hounds-unwrap.md new file mode 100644 index 0000000..99b5b7e --- /dev/null +++ b/.changeset/brave-hounds-unwrap.md @@ -0,0 +1,5 @@ +--- +"@germ-network/atprototypes": patch +--- + +Fix the build: `AtprotoTypesVerify`'s `RepoSigningKey` still unwrapped `DIDDocument.verificationMethod` and `VerificationMethod.publicKeyMultibase` as non-optional after they became optional in the canonical-schema relaxation. Both absent cases now refuse with `noAtprotoSigningKey`, matching the existing empty-list behaviour, with regression tests for each. diff --git a/Sources/AtprotoTypesVerify/Signing/RepoSigningKey.swift b/Sources/AtprotoTypesVerify/Signing/RepoSigningKey.swift index ca59449..d59071b 100644 --- a/Sources/AtprotoTypesVerify/Signing/RepoSigningKey.swift +++ b/Sources/AtprotoTypesVerify/Signing/RepoSigningKey.swift @@ -49,8 +49,11 @@ public struct RepoSigningKey: Sendable { ///fragment, and taking "the first one" instead would let a document with an ///extra method up front decide what we check against. public init(atprotoKeyIn document: Atproto.DIDDocument, did: Atproto.DID) throws { + //`verificationMethod` is optional in the canonical DID schema, so an + //absent list and an empty one mean the same thing here: nothing to + //check against, which is a refusal rather than a pass. guard - let method = document.verificationMethod.first(where: { + let method = document.verificationMethod?.first(where: { $0.id == "#atproto" || $0.id.hasSuffix("#atproto") }) else { @@ -68,7 +71,13 @@ public struct RepoSigningKey: Sendable { throw Atproto.Repo.ProofError.signingKeyControllerMismatch } - try self.init(multibase: method.publicKeyMultibase) + //A method that publishes no key material proves nothing, so it is the + //same refusal as having no `#atproto` method at all — never a skip. + guard let multibase = method.publicKeyMultibase else { + throw Atproto.Repo.ProofError.noAtprotoSigningKey + } + + try self.init(multibase: multibase) } public init(multibase: String) throws { diff --git a/Tests/AtprotoTypesVerifyTests/RepoProofVerifierTests.swift b/Tests/AtprotoTypesVerifyTests/RepoProofVerifierTests.swift index 95761f2..3255cc5 100644 --- a/Tests/AtprotoTypesVerifyTests/RepoProofVerifierTests.swift +++ b/Tests/AtprotoTypesVerifyTests/RepoProofVerifierTests.swift @@ -336,6 +336,50 @@ struct RepoProofVerifierTests { } } + ///`verificationMethod` and `publicKeyMultibase` are both optional in the + ///canonical DID schema, so a document can omit either entirely. Absent has + ///to mean the same refusal as empty — these two cases are only reachable + ///by hand-written JSON, so without them the nil branches are decided by + ///whoever last touched the unwrap rather than by a test. + @Test("a document omitting verificationMethod entirely is refused") + func refusesDocumentWithNoVerificationMethodKey() throws { + let scenario = Scenario() + let document = try JSONDecoder().decode( + Atproto.DIDDocument.self, + from: Data(#"{"id":"\#(scenario.did.rawValue)"}"#.utf8) + ) + + #expect(throws: Atproto.Repo.ProofError.noAtprotoSigningKey) { + try Atproto.Repo.Verifier().verifyRecordProof( + car: try scenario.car(), + did: scenario.did, + path: Self.path, + document: document + ) + } + } + + @Test("an atproto method publishing no key material is refused") + func refusesMethodWithoutMultibase() throws { + let scenario = Scenario() + let json = """ + {"id":"\(scenario.did.rawValue)","verificationMethod":[\ + {"id":"\(scenario.did.rawValue)#atproto","type":"Multikey",\ + "controller":"\(scenario.did.rawValue)"}]} + """ + let document = try JSONDecoder().decode( + Atproto.DIDDocument.self, from: Data(json.utf8)) + + #expect(throws: Atproto.Repo.ProofError.noAtprotoSigningKey) { + try Atproto.Repo.Verifier().verifyRecordProof( + car: try scenario.car(), + did: scenario.did, + path: Self.path, + document: document + ) + } + } + ///The gap this closes on the shipped fetch path: today's `resolveMiniDoc` ///adapter builds a document with `verificationMethod: []`, so every proof ///would stop here until that key is carried through.