diff options
Diffstat (limited to 'services/docker/mail-server/relay/aws/app.ts')
-rw-r--r-- | services/docker/mail-server/relay/aws/app.ts | 103 |
1 files changed, 103 insertions, 0 deletions
diff --git a/services/docker/mail-server/relay/aws/app.ts b/services/docker/mail-server/relay/aws/app.ts index 51da795..76e2793 100644 --- a/services/docker/mail-server/relay/aws/app.ts +++ b/services/docker/mail-server/relay/aws/app.ts @@ -1,4 +1,5 @@ import { parseArgs } from "@std/cli"; +import { decodeBase64 } from "@std/encoding/base64"; import { z } from "zod"; import { zValidator } from "@hono/zod-validator"; @@ -9,6 +10,89 @@ import { AwsMailDeliverer } from "./deliver.ts"; import { AwsMailRetriever } from "./retriever.ts"; import config from "../config.ts"; +const AWS_SNS_MESSAGE_MODEL = z.object({ + Type: z.enum(["Notification", "SubscriptionConfirmation"]), + TopicArn: z.string(), + Timestamp: z.string(), + Subject: z.string().optional(), + SubscribeURL: z.string().optional(), + Message: z.string(), + MessageId: z.string(), + Signature: z.string(), + SigningCertURL: z.string(), + SignatureVersion: z.string(), +}); + +type AwsSnsMessage = z.TypeOf<typeof AWS_SNS_MESSAGE_MODEL>; + +const AWS_SES_SNS_MESSAGE_MODEL = z.object({ + notificationType: z.literal("Received").or(z.string()), + receipt: z.object({ + recipients: z.array(z.string()), + }), + mail: z.object({ + messageId: z.string(), + }), +}); + +const AWS_SNS_SIGNATURE_FIELDS = { + Notification: [ + "Message", + "MessageId", + "Subject", + "Timestamp", + "TopicArn", + "Type", + ], + SubscriptionConfirmation: [ + "Message", + "MessageId", + "SubscribeURL", + "Timestamp", + "TopicArn", + "Type", + ], +} as const; + +async function verifySnsSignature(message: AwsSnsMessage) { + const signingCertUrl = message.SigningCertURL; + + if (!new URL(signingCertUrl).hostname.endsWith(".amazonaws.com")) { + throw new Error( + `Signature cert url ${signingCertUrl} does not belong to aws!!!`, + ); + } + + const signature = message.Signature; + const data = AWS_SNS_SIGNATURE_FIELDS[message.Type].filter((field) => + field in message + ).flatMap((field) => [field, message[field]]).join("\n"); + const certData = await (await fetch(signingCertUrl)).bytes(); + const key = await crypto.subtle.importKey( + "pkcs8", + certData, + { + name: "RSA-PSS", + hash: message.SignatureVersion === "1" ? "SHA-1" : "SHA-256", + }, + false, + ["verify"], + ); + const isVerified = await crypto.subtle.verify( + { + name: "RSA-PSS", + hash: message.SignatureVersion === "1" ? "SHA-1" : "SHA-256", + }, + key, + decodeBase64(signature), + new TextEncoder().encode(data), + ); + + if (!isVerified) { + throw new Error("Signature does not match!!!"); + } +} + export class AwsRelayApp extends AppBase { readonly #aws = new AwsContext(); readonly #retriever; @@ -35,6 +119,25 @@ export class AwsRelayApp extends AppBase { }); }, ); + this.hono.post( + `/receive/aws-sns/${config.getValue("awsInboundPath")}`, + zValidator("json", AWS_SNS_MESSAGE_MODEL), + async (ctx) => { + const message = ctx.req.valid("json"); + await verifySnsSignature(message); + if (message.Type === "Notification") { + const sesMessage = JSON.parse(message.Message); + const parsedSesMessage = AWS_SES_SNS_MESSAGE_MODEL.parse(sesMessage); + // TODO: Here!!! Specify receipts! + await this.#retriever.deliverS3Mail(parsedSesMessage.mail.messageId); + return ctx.json({ + "msg": "Done!", + }); + } else if (message.Type === "SubscriptionConfirmation") { + } else { + } + }, + ); } realServe() { |