fix(migrate): oauth account:* spec, p256 support

Lewis: May this revision serve well! <lu5a@proton.me>
This commit is contained in:
Lewis
2026-05-30 21:07:16 +03:00
parent 38508c2c6e
commit 4e2525b245
4 changed files with 73 additions and 3 deletions
+1 -1
View File
@@ -8,7 +8,7 @@
],
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"],
"scope": "atproto transition:generic repo:* blob:*/* rpc:* rpc:com.atproto.server.createAccount?aud=* account:*?action=manage identity:*",
"scope": "atproto transition:generic repo:* blob:*/* rpc:* rpc:com.atproto.server.createAccount?aud=* account:*?action=manage account:repo?action=manage identity:*",
"token_endpoint_auth_method": "none",
"application_type": "web",
"dpop_bound_access_tokens": true
@@ -209,11 +209,15 @@ export function createOfflineInboundMigrationFlow() {
}
try {
userRotationKeypair = await plcOps.getKeyPair(state.rotationKey.trim());
const { lastOperation } = await plcOps.getLastPlcOpFromPlc(state.userDid);
const currentRotationKeys = lastOperation.rotationKeys || [];
if (!currentRotationKeys.includes(userRotationKeypair.didPublicKey)) {
userRotationKeypair = await plcOps.getMatchingKeyPair(
state.rotationKey.trim(),
currentRotationKeys,
);
if (!userRotationKeypair) {
state.rotationKeyDidKey = "";
return false;
}
+27
View File
@@ -248,6 +248,33 @@ export class PlcOps {
};
}
async getMatchingKeyPair(
privateKeyString: string,
acceptableDidKeys: readonly string[],
): Promise<KeypairInfo | null> {
const curves: readonly KeyCurve[] = ["secp256k1", "p256"];
const results = await Promise.allSettled(
curves.map((curve) => this.getKeyPair(privateKeyString, curve)),
);
const candidates = results
.filter(
(r): r is PromiseFulfilledResult<KeypairInfo> =>
r.status === "fulfilled",
)
.map((r) => r.value);
if (candidates.length === 0) {
const rejection = results.find(
(r): r is PromiseRejectedResult => r.status === "rejected",
);
throw rejection?.reason ?? new Error("Unrecognized key format");
}
return (
candidates.find((info) =>
acceptableDidKeys.includes(info.didPublicKey)
) ?? null
);
}
async signAndPublishNewOp(
did: string,
signingRotationKey: PrivateKey,
@@ -144,6 +144,45 @@ describe("migration/plc-ops", () => {
});
});
describe("getMatchingKeyPair", () => {
it("resolves a P-256 key supplied as hex against its did:key", async () => {
const keypair = await P256PrivateKeyExportable.createKeypair();
const rawHex = await keypair.exportPrivateKey("rawHex");
const did = await keypair.exportPublicKey("did");
const result = await plcOps.getMatchingKeyPair(rawHex, [did]);
expect(result?.didPublicKey).toBe(did);
});
it("resolves a secp256k1 key supplied as hex against its did:key", async () => {
const keypair = await Secp256k1PrivateKeyExportable.createKeypair();
const rawHex = await keypair.exportPrivateKey("rawHex");
const did = await keypair.exportPublicKey("did");
const result = await plcOps.getMatchingKeyPair(rawHex, [did]);
expect(result?.didPublicKey).toBe(did);
});
it("returns null when no curve matches the accepted keys", async () => {
const keypair = await P256PrivateKeyExportable.createKeypair();
const rawHex = await keypair.exportPrivateKey("rawHex");
const result = await plcOps.getMatchingKeyPair(rawHex, [
"did:key:zQ3shqPwo8CSE8zNXEyEpN4ASEBCCNeUFQq8Lrw3zkAJYB7SB",
]);
expect(result).toBeNull();
});
it("throws on unparseable input", async () => {
await expect(
plcOps.getMatchingKeyPair("not-a-valid-key", []),
).rejects.toThrow();
});
});
describe("getKeyPair - JWK format", () => {
it("imports secp256k1 JWK with d parameter", async () => {
const keypair = await Secp256k1PrivateKeyExportable.createKeypair();