fix(ipsec): удалять сертификат только по явной привязке
This commit is contained in:
1 parent
d52c48e5ed
commit
057b530364
16 files changed
+644
-22
No files matched your search
@@ -6,9 +6,9 @@ import { MikrotikClient, MikrotikError } from "../services/mikrotik.js"
|
||||
import { fetchIpsecState } from "../services/ipsec-live.js"
|
||||
const server = { id: 1, name: "test", host: "192.0.2.1", enabled: true } as Parameters<typeof fetchIpsecState>[0]
|
||||
const original = MikrotikClient.fromServer
|
||||
let writes = 0, records = 0, fault = "", certificateFixture = false
|
||||
let writes = 0, records = 0, fault = "", certificateFixture = false, removedCertificates = 0
|
||||
const identities: Record<string,string>[] = [{ ".id": "*I", peer: "ike", "auth-method": "pre-shared-key", "remote-id": "key-id:alice", "mode-config": "mc-ipsec-alice", comment: "MikrotikManager:ipsec user=Alice" }, { ".id": "*OTHER", peer: "ike", "auth-method": "pre-shared-key", "remote-id": "key-id:other", "mode-config": "mc-ipsec-other", comment: "MikrotikManager:ipsec user=Other" }]
|
||||
MikrotikClient.fromServer = () => ({ get: async (p: string) => certificateFixture && p === "/certificate" ? [{ ".id": "*CERT", name: "ipsec-user-shared", "common-name": "shared", "key-usage": "tls-client" }] : p === "/ip/ipsec/peer" ? [{ ".id": "*P", name: "ike", "exchange-mode": "ike2" }] : p === "/ip/ipsec/identity" ? identities : p === "/ip/ipsec/mode-config" ? [{ ".id": "*MC", name: "mc-ipsec-alice", address: "198.18.81.250" }, { ".id": "*OTHERMC", name: "mc-ipsec-other", address: "198.18.81.249" }] : [], put: async () => { writes++ }, post: async () => { writes++ }, delete: async () => { writes++ }, patch: async (path: string, body: Record<string,string>) => { writes++; if (fault === "partial" && writes === 2) throw new MikrotikError(403, path, "rejected"); Object.assign(identities.find(i => path.endsWith(i[".id"]))!, body); if (fault === "lost-ack") throw Error("socket closed after application") } }) as unknown as MikrotikClient
|
||||
MikrotikClient.fromServer = () => ({ get: async (p: string) => certificateFixture && p === "/certificate" ? [{ ".id": "*CERT", name: "ipsec-user-shared", "common-name": "shared", "key-usage": "tls-client" }] : p === "/ip/ipsec/peer" ? [{ ".id": "*P", name: "ike", "exchange-mode": "ike2" }] : p === "/ip/ipsec/identity" ? identities : p === "/ip/ipsec/mode-config" ? [{ ".id": "*MC", name: "mc-ipsec-alice", address: "198.18.81.250" }, { ".id": "*OTHERMC", name: "mc-ipsec-other", address: "198.18.81.249" }] : [], removeCertificate: async () => { writes++; removedCertificates++ }, put: async () => { writes++ }, post: async () => { writes++ }, delete: async () => { writes++ }, patch: async (path: string, body: Record<string,string>) => { writes++; if (fault === "partial" && writes === 2) throw new MikrotikError(403, path, "rejected"); Object.assign(identities.find(i => path.endsWith(i[".id"]))!, body); if (fault === "lost-ack") throw Error("socket closed after application") } }) as unknown as MikrotikClient
|
||||
const app = Fastify(); app.setValidatorCompiler(validatorCompiler); app.setSerializerCompiler(serializerCompiler)
|
||||
await app.register(createIpsecRoutes({ findServer: async () => server, recordRevision: async () => { records++; return {created:false,revision:{} as never} } }), { prefix: "/api" })
|
||||
const patch = (payload: Record<string, unknown>) => app.inject({ method: "PATCH", url: "/api/ipsec/users/1/identity:*I", payload })
|
||||
@@ -18,6 +18,16 @@ writes=records=0; fault="partial"; let r=await patch({name:"New",psk:"new-test-s
|
||||
writes=records=0; fault="lost-ack"; r=await patch({name:"Unknown"}); assert.equal(r.statusCode,502); assert.equal(r.json().state,"unknown"); assert.equal(r.json().confirmedWrites,0); assert.equal(identities[0]!.comment,"MikrotikManager:ipsec user=Unknown"); assert.equal(records,0)
|
||||
writes=records=0; fault=""; r=await patch({remoteId:"fresh"}); assert.equal(r.statusCode,200); assert.equal(identities[0]!["remote-id"],"key-id:fresh"); assert.equal(writes,1); assert.equal(records,1)
|
||||
|
||||
|
||||
|
||||
// A same-named certificate is a separate object, not a binding inferred from a PSK label.
|
||||
certificateFixture=true;identities.splice(0,identities.length,{".id":"*UNBOUND",peer:"ike","auth-method":"pre-shared-key","remote-id":"key-id:unbound",comment:"MikrotikManager:ipsec user=shared"});
|
||||
writes=records=removedCertificates=0;
|
||||
let deletion=await app.inject({method:"DELETE",url:"/api/ipsec/users/1/identity:*UNBOUND"});assert.equal(deletion.statusCode,200);assert.equal(writes,1);assert.equal(removedCertificates,0);assert.equal(records,1);
|
||||
// Explicit certificate binding still permits deletion when no other references exist.
|
||||
identities.splice(0,identities.length,{".id":"*BOUND",peer:"ike","auth-method":"digital-signature","remote-certificate":"ipsec-user-shared",comment:"MikrotikManager:ipsec user=shared"});
|
||||
writes=records=removedCertificates=0;
|
||||
deletion=await app.inject({method:"DELETE",url:"/api/ipsec/users/1/identity:*BOUND"});assert.equal(deletion.statusCode,200);assert.equal(writes,2);assert.equal(removedCertificates,1);assert.equal(records,1);
|
||||
certificateFixture=true;identities.splice(0,identities.length,...["*FIRST","*SECOND"].map((id,n)=>({".id":id,peer:"ike","auth-method":"digital-signature","remote-certificate":"ipsec-user-shared",comment:"MikrotikManager:ipsec user=Shared"+n})));
|
||||
for(const method of ["PATCH","DELETE"] as const){writes=records=0;const r=await app.inject({method,url:"/api/ipsec/users/1/cert:*CERT",...(method==="PATCH"?{payload:{name:"Wrong"}}:{})});assert.equal(r.statusCode,409);assert.equal(writes,0);assert.equal(records,0)}
|
||||
writes=records=0;r=await app.inject({method:"DELETE",url:"/api/ipsec/users/1/identity:*FIRST"});assert.equal(r.statusCode,409);assert.equal(writes,0);assert.equal(records,0);
|
||||
|
||||
@@ -535,10 +535,6 @@ export function createIpsecRoutes(dependencies = ipsecDefaults): FastifyPluginAs
|
||||
const identity = target.identity ?? (target.certRow ? state.identities.find(i => i["remote-certificate"] === target.certRow?.name) : undefined)
|
||||
const certRow = identity ? undefined : target.certRow
|
||||
if (!identity && !certRow) return reply.status(404).send({ error: "Клиент не найден" })
|
||||
const managed = identity
|
||||
? isIpsecManagedComment(identity.comment)
|
||||
: String(certRow?.name ?? "").startsWith("ipsec-user-")
|
||||
const userName = identity ? (parseIpsecUserComment(identity.comment) ?? "") : ""
|
||||
const sharedMcName = (state.modeConfigs.find((m) => (m.name ?? "").trim() === IPSEC_COMMON_NAME)?.name ?? "").trim()
|
||||
const personal = identity ? (identity["mode-config"] ?? "").trim() : ""
|
||||
const remoteCert = identity
|
||||
@@ -546,7 +542,8 @@ export function createIpsecRoutes(dependencies = ipsecDefaults): FastifyPluginAs
|
||||
: String(certRow?.name ?? "").trim()
|
||||
|
||||
// Refuse before deleting the identity: a shared certificate must survive.
|
||||
const deletionCertName = remoteCert || (managed && userName ? clientCertName(userName) : "")
|
||||
// A display name does not establish ownership of a certificate.
|
||||
const deletionCertName = remoteCert
|
||||
if (removeCertificate && deletionCertName && (
|
||||
state.identities.some(i => i[".id"] !== identity?.[".id"] && ((i["remote-certificate"] ?? "").trim() === deletionCertName || (i.certificate ?? "").trim() === deletionCertName))
|
||||
|| state.peers.some(p => (p.certificate ?? "").trim() === deletionCertName)
|
||||
@@ -561,7 +558,7 @@ export function createIpsecRoutes(dependencies = ipsecDefaults): FastifyPluginAs
|
||||
const ike2Peers = selectIke2Peers(state.peers)
|
||||
const caName = resolveIke2CaName(state.certs, resolveIke2ServerCert(state.certs, ike2Peers, state.identities))
|
||||
const serverCertName = String(resolveIke2ServerCert(state.certs, ike2Peers, state.identities)?.name ?? "").trim()
|
||||
const certName = remoteCert || (managed && userName ? clientCertName(userName) : "")
|
||||
const certName = remoteCert
|
||||
if (certName && certName !== caName && certName !== serverCertName) {
|
||||
await client.removeCertificate(certName)
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user