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.