AS4 в .NET без Java-шлюза: ebMS 3.0, WS-Security и неотрекаемые квитанции шагом маршрута
Если искать бытовую аналогию, AS4 это заказное письмо с уведомлением о вручении, только вместо бумаги по сети передаётся сам документ. Вы отправляете его партнёру, а партнёр подтверждает получение. Разница с почтой в том, что бумажное уведомление говорит лишь, что конверт дошёл, а квитанция AS4 криптографически привязана к содержимому: она подтверждает, что дошёл именно этот документ, байт в байт.
Если вы обмениваетесь документами с европейским B2B, вы, скорее всего, уже работаете по AS4. Это профиль OASIS ebMS 3.0, на котором стоит EU eDelivery: правовые документы в e-CODEX и e-Justice, электронные счета в сети Peppol, заявки газовых операторов в ENTSOG, электронные транспортные накладные в eFTI. AS4 это прямой преемник AS2: вместо S/MIME-конверта по сети идёт SOAP 1.2 с заголовком eb:Messaging, WS-Security и подписанная квитанция. Там, где у AS2 был MDN с MIC, у AS4 eb:Receipt с non-repudiation: доказательство, что партнёр получил ровно тот документ, который вы подписали.
В .NET этот мир до сих пор закрыт плохо. Зрелые реализации AS4 живут в Java: Holodeck B2B, Apache Domibus (эталонный узел EU eDelivery), phase4. Значит, рядом с вашим .NET-бэкендом ставится либо отдельный JVM-процесс, либо коммерческий шлюз со своей лицензией, своим каталогом входящих документов и отдельной командой сопровождения. Базовый .NET-стек AS4 не закрывает: ни SOAP-конверта с ebMS-заголовками, ни WS-Security над вложениями MIME, ни квитанций, которые можно предъявить.
redb.Route.As4 ставит AS4 внутрь маршрута. Отправка идёт через To, приём через From. Документ собрался, сжался, подписался, зашифровался, ушёл партнёру, вернулась проверенная квитанция. Или: конверт пришёл, расшифровался, подпись проверилась, документ лёг в маршрут, а партнёру ушла квитанция. Один процесс, один деплой, одно дерево трейсов. Дальше про то, как это выглядит в коде и где на самом деле лежит сложность.
Почему AS4, а не «просто HTTPS»
Резонный вопрос: если канал и так под TLS, зачем поверх него ещё подписывать и шифровать? Ответ тот же, что в мире AS2, и он не про паранойю. TLS защищает канал: он живёт от вашего сокета до сокета партнёра и заканчивается, как только байты легли на диск. В логе балансировщика, в архиве прокси, в файловой системе получателя документ уже открыт. AS4 защищает сам документ: он остаётся подписанным и зашифрованным на всём пути и в покое, а расшифровать его может только владелец приватного ключа.
Но главное не это. У TLS нет расписки. AS4 её даёт: получатель отвечает подписанной квитанцией, и её ссылки на дайджесты (non-repudiation references) указывают ровно на то, что подписали вы. Это и есть неотрекаемость: когда за документом стоят деньги, сроки или правовые обязательства, вам нужен не просто факт доставки, а доказательство доставки именно этого документа. Именно поэтому AS4 обязателен там, где документ несёт юридическое обязательство, а не письмо.
AS4 за минуту
По сети это HTTP POST с multipart/related и SOAP 1.2. Конверт состоит из двух частей.
Заголовок. eb:Messaging несёт бизнес-метаданные: eb:UserMessage с идентификатором сообщения, сторонами (eb:PartyId), сервисом и действием (eb:Service, eb:Action), а также четыре обязательных свойства четырёх углов (originalSender, finalRecipient). Рядом блок wsse:Security: wsu:Timestamp, сертификат отправителя (wsse:BinarySecurityToken), зашифрованный ключ сессии (xenc:EncryptedKey), подпись (ds:Signature) и по одному xenc:EncryptedData на каждое зашифрованное вложение.
Вложения. Бизнес-документ не в теле SOAP, а отдельными MIME-частями (SOAP with Attachments, SwA). Часть сжимается gzip, затем шифруется, затем всё сообщение подписывается. Тело SOAP при этом обычно пустое. Это ключевое отличие от нашего SOAP-коннектора, который умеет MTOM: в AS4 payload всегда едет вложением.
Получатель расшифровывает своим приватным ключом, проверяет подпись сертификатом отправителя, распаковывает и в том же HTTP-ответе возвращает подписанную квитанцию. Синхронная квитанция обязательна: этого требует профиль eDelivery AS4, отправитель ждёт её сразу, а не отдельным запросом.
Читаемый маршрут: endpoint как строка
redb.Route это Apache Camel под .NET: маршрут описывается как From → … → To, а endpoint это URI-строка. Коннектор AS4 добавляет две схемы, as4 (HTTP) и as4s (HTTPS), и они читаются как предложение:
as4s://ap.partner.example/as4?connectionFactory=node&partner=acme # куда отправляем
as4:/as4/in?host=0.0.0.0&port=4090&connectionFactory=node # где принимаем
По строке сразу видно намерение: куда идём, на каком порту слушаем, какой узел и партнёр. Есть и флюентный билдер, который компилируется в тот же URI, так что адрес эндпоинта спокойно берётся из appsettings.json и меняется по окружениям без правки кода.
services.AddRedbRoute(route =>
{
route.Services.AddRedbRouteAs4();
route.AddRouteBuilder<MyRoutes>();
});
AddRedbRouteAs4() регистрирует схемы. Приём AS4 работает на общем Kestrel-хосте, который делят все HTTP-коннекторы процесса: HTTP, SOAP, AS2 и AS4 в одном воркере не дерутся за порт, каждый слушает свой путь. Сертификаты, пароли и алгоритмы в URI не живут: URI это ключ маршрута, он попадает в логи, трейсы и панель наблюдаемости. Всё секретное выносится в отдельный объект.
Ту же интеграцию описывают не только кодом, но и разметкой Route-XML: эндпоинты в файлах .route.xml,
узел и партнёры в <bean>. Разметку удобно писать в VS Code с расширением redb-route-xml:
автодополнение и валидация по схеме, дерево маршрутов, граф шагов. В Marketplace расширения нет,
его отдают файлом в релизах: скачайте redb-route-xml-<version>.vsix со страницы релизов и поставьте
через «Extensions → … → Install from VSIX» или командой
code --install-extension redb-route-xml-<version>.vsix (базовый Red Hat XML VS Code обычно подтянет
сам). Дальше рядом с каждым примером на C# приведён эквивалент в XML, а полные примеры разметки лежат
в репозитории: demos/XmlDemo и demos/SerialNumbersDemo/SerialNumbers.Xml.
Узел и партнёры: один объект вместо россыпи параметров
В AS4 всё держится на соглашении (P-Mode): кто, что, кому, какими алгоритмами, нужна ли квитанция. У нас соглашение это типизированный объект в реестре. Наш узел (As4ConnectionFactory) знает нашу идентичность, наши ключи и список партнёров; каждый партнёр (As4Partner) это одно соглашение.
context.AddToRegistry("node", new As4ConnectionFactory
{
OurPartyId = "urn:oasis:names:tc:ebcore:partyid-type:unregistered:us",
ExternalHostName = "ap.us.example", // хост в наших eb:MessageId, не имя машины
SigningCertificate = ourPfx, // наш ключ: подписывает исходящее и квитанции
DecryptionCertificates = { ourPfx }, // наши ключи: расшифровывают входящее
Partners =
{
new As4Partner
{
Name = "acme",
PartyId = "urn:oasis:names:tc:ebcore:partyid-type:unregistered:acme",
Service = "urn:example:services:invoice",
Action = "Submit",
PartnerSigningCertificates = { acmeCer }, // проверяет подпись партнёра
PartnerEncryptionCertificate = acmeCer, // шифрует ему payload
},
},
});
Маршрут ссылается на узел по имени (connectionFactory=node) и, при отправке, на партнёра (partner=acme). Приёмная точка партнёра не называет: один URL приёма принимает всех партнёров узла, а конкретного отправителя коннектор определяет по содержимому конверта. Наличие сертификата в списке не означает «валидность вообще»: подпись принимается только от партнёрского сертификата и только если она покрывает то, что требует профиль. Ротация ключа это список из нескольких сертификатов, а не остановка обмена.
Тот же узел и партнёр разметкой (context.xml):
<context xmlns="urn:redb:route:1.0">
<bean name="acme" type="redb.Route.As4.As4Partner, redb.Route.As4">
<property key="Name" value="acme"/>
<property key="PartyId" value="urn:oasis:names:tc:ebcore:partyid-type:unregistered:acme"/>
<property key="Service" value="urn:example:services:invoice"/>
<property key="Action" value="Submit"/>
<property key="PartnerSigningCertificates">
<list>
<bean type="System.Security.Cryptography.X509Certificates.X509CertificateLoader, System.Security.Cryptography"
factoryMethod="LoadCertificateFromFile">
<constructorArg value="{{as4.certificates}}/acme.cer"/>
</bean>
</list>
</property>
<property key="PartnerEncryptionCertificate">
<bean type="System.Security.Cryptography.X509Certificates.X509CertificateLoader, System.Security.Cryptography"
factoryMethod="LoadCertificateFromFile">
<constructorArg value="{{as4.certificates}}/acme.cer"/>
</bean>
</property>
</bean>
<bean name="node-key"
type="System.Security.Cryptography.X509Certificates.X509CertificateLoader, System.Security.Cryptography"
factoryMethod="LoadPkcs12FromFile">
<constructorArg value="{{as4.certificates}}/us.pfx"/>
<constructorArg value="{{as4.password}}"/>
</bean>
<bean name="node" type="redb.Route.As4.As4ConnectionFactory, redb.Route.As4">
<property key="OurPartyId" value="urn:oasis:names:tc:ebcore:partyid-type:unregistered:us"/>
<property key="ExternalHostName" value="ap.us.example"/>
<property key="SigningCertificate" ref="node-key"/>
<property key="DecryptionCertificates"><list><ref bean="node-key"/></list></property>
<property key="Partners"><list><ref bean="acme"/></list></property>
</bean>
<bean name="as4-in" type="redb.Route.Processors.InMemoryIdempotentRepository, redb.Route"/>
</context>
Отправка: сжали, зашифровали, подписали, дождались квитанции
Отправка это шаг To. Тело становится одним вложением: byte[] как есть, string в UTF-8, Stream копируется потоком.
using redb.Route.As4.Fluent;
From("direct://outbound")
.SetHeader(As4Headers.OriginalSender, "urn:example:c1") // четыре угла: кто прислал
.SetHeader(As4Headers.FinalRecipient, "urn:example:c4") // и кто конечный получатель
.To(As4.Send("https://ap.acme.example/as4")
.ConnectionFactory("node")
.Partner("acme"));
Разметкой тот же отправляющий маршрут:
<!-- routes/outbox.route.xml -->
<routes xmlns="urn:redb:route:1.0">
<route id="as4-outbox">
<from uri="direct://outbound"/>
<setHeader name="redbAs4.property.originalSender" value="urn:example:c1"/>
<setHeader name="redbAs4.property.finalRecipient" value="urn:example:c4"/>
<to uri="as4s://ap.acme.example/as4?connectionFactory=node&partner=acme"/>
</route>
</routes>
За строкой To идут gzip, шифрование ключом партнёра, подпись вашим ключом, POST и разбор ответа. Продюсер ждёт квитанцию и проверяет её: подписана одним из сертификатов партнёра и ссылается ровно на те дайджесты, что подписали мы. Результат кладётся на exchange.Out, чтобы маршрут мог принять решение:
Заголовок на exchange.Out |
Что значит |
|---|---|
redbAs4.receiptValid |
bool: квитанция получена и проверена, включая non-repudiation |
redbAs4.receiptMessageId |
eb:MessageId самой квитанции |
redbAs4.messageId |
eb:MessageId отправленного сообщения |
Исключения вместо ручного разбора ответа:
| Исход | Исключение |
|---|---|
| Партнёр ответил ebMS-ошибкой | As4ErrorSignalException (ErrorCode, Description, ErrorDetail) |
Квитанции в ответе нет или ответа нет за timeout |
As4ReceiptException (EBMS:0301) |
| Квитанция с чужой подписью или другими дайджестами | As4ReceiptException (EBMS:0302) |
Приём: расшифровать, проверить, отдать маршруту, ответить квитанцией
Приём это шаг From, то есть источник маршрута.
From(As4.Receive("/as4/in").Host("0.0.0.0").Port(4090)
.ConnectionFactory("node").IdempotentRepository("as4-in"))
.ValidateXsd(invoiceSchema) // проверка бизнес-документа по XSD
.To("direct://process-invoice");
Разметкой тот же принимающий маршрут:
<!-- routes/inbox.route.xml -->
<routes xmlns="urn:redb:route:1.0">
<route id="as4-inbox">
<from uri="as4:/as4/in?host=0.0.0.0&port=4090&connectionFactory=node&idempotentRepository=as4-in"/>
<validateXsd file="invoice.xsd"/>
<to uri="direct://process-invoice"/>
</route>
</routes>
Пришедший конверт коннектор расшифровывает, проверяет подпись, распаковывает и сопоставляет с партнёром. В маршрут попадает чистый бизнес-документ с правильным Message.ContentType (например, application/xml или application/edi-x12), а не транспортная обёртка. Метаданные обмена лежат под redbAs4.*:
| Заголовок | Что значит |
|---|---|
redbAs4.partner |
имя партнёра, которому сопоставлено сообщение |
redbAs4.signatureValid / signerThumbprint |
подпись проверена сертификатом партнёра; его отпечаток |
redbAs4.fromPartyId / toPartyId |
стороны сообщения |
redbAs4.service / action |
бизнес-операция из eb:CollaborationInfo |
redbAs4.messageId |
eb:MessageId входящего сообщения |
Тут есть важная деталь: квитанция уходит партнёру после того, как отработал маршрут. Если ваш To упал и транзакция откатилась, успешной квитанции не будет: иначе вы бы подтвердили приём документа, который не сохранили. Отказ маршрута превращается в ebMS-ошибку (EBMS:0004), и отправитель повторит передачу.
Надёжность из движка: дубли и повтор тем же сообщением
Профиль требует дедупликации, надёжности передачи (Reception Awareness) и повторов (Retry). В redb.Route это не пишется заново: коннектор встаёт на готовые механизмы движка и своего цикла повторов, своего хранилища или своего счётчика не заводит.
На приёме idempotentRepository обязателен. eb:MessageId входящего сообщения захватывается только после всех проверок безопасности (подделанное сообщение не может занять id настоящего), подтверждается при успехе маршрута и освобождается при отказе. Пришёл дубль: партнёру снова уходит квитанция, но в маршрут сообщение повторно не отдаётся. Хранилище бывает разным, и от выбора зависит, что переживает рестарт: InMemoryIdempotentRepository забывает всё при перезапуске и не видит повтор, пришедший на другую ноду за балансировщиком; для продакшна есть redb- или SQL-хранилище. Окно дедупликации это Ttl хранилища, и оно должно быть длиннее всего расписания повторов отправителя.
На отправке повтор это OnException. Продюсер ставит redbAs4.messageId на обмен до первой попытки и держит подписанный запрос на обмене: повторная отправка шлёт те же байты, тот же id и ту же подпись. Так требует протокол: по AS4 повторная передача это то же самое сообщение. Партнёр распознаёт дубль и отвечает квитанцией, которую сохранил для первой передачи (так делает Domibus), а её дайджесты указывают на ту самую подпись, которую мы повторили.
OnException<As4ReceiptException>()
.MaximumRedeliveries(5)
.RedeliveryDelay(TimeSpan.FromSeconds(30))
.UseExponentialBackOff()
.Handled()
.To("direct://as4-undelivered"); // dead letter после последней попытки
Разметкой тот же обработчик (секция файла, применяется ко всем его маршрутам):
<onException exceptions="redb.Route.As4.As4ReceiptException, redb.Route.As4"
handled="true" maximumRedeliveries="5" redeliveryDelay="00:00:30"
exponentialBackOff="true">
<to uri="direct://as4-undelivered"/>
</onException>
Повтор движка живёт в памяти: рестарт теряет обмен, который повторяли. Для расписаний на часы документ держат в собственном outbox (объект redb с сохранённым redbAs4.messageId) и досылают timer:-маршрутом то, на что квитанции ещё нет.
Потоковость: большие документы это норма
В B2B попадаются вложения на сотни мегабайт, и держать их целиком в памяти нельзя. Отправляемое вложение уходит в сеть через потоковый кэш ядра: крупный payload проходит через временный файл, а не через byte[]. Сжатие и шифрование потоковые (потоковый AES-GCM на BouncyCastle), приёмное вложение можно получить как поток (streamBody), а не как развёрнутый в память массив.
Есть и границы: размер тела запроса (maxRequestBodySize, по умолчанию 100 МБ), размер SOAP-конверта (maxEnvelopeCharacters, 1 МБ; payload едет вложениями и в этот лимит не входит) и размер ответа партнёра (maxResponseBodySize, 4 МБ; квитанция это килобайты). Форвард-стрим, который нельзя перечитать, читается один раз: чтобы повторять отправку, его кэшируют в маршруте через .StreamCaching().
Крипто-слой, который пришлось написать самим
В этом и сложность AS4, и причина, по которой готовой реализации в мейнстриме .NET нет. Штатного пути не нашлось, и это не лень стека, а конкретные дыры в API.
SignedXml из .NET не умеет разрешать ссылки cid: на вложения MIME. Значит, подпись обрабатывается вручную: коннектор сам разбирает ds:Reference и SignedInfo, применяет Attachment-Content-Signature-Transform и Exclusive C14N (через публичный XmlDsigExcC14NTransform), подписывает RSA PKCS#1 v1.5.
EncryptedXml из .NET не умеет AES-GCM, а его DecryptKey поддерживает только OAEP-SHA1, тогда как профиль требует OAEP с MGF1-SHA256. Значит, ключ сессии расшифровывается самим, а данные шифруются через System.Security.Cryptography.AesGcm по правилам XML Encryption 1.1 (IV 12 байт, тег 16, порядок «IV, шифротекст, тег»).
Недоверенный XML идёт только через SafeXml ядра: DTD на входе запрещён (сама SOAP 1.2 его не допускает), конверт ограничен по размеру. От подмены подписанного элемента (signature wrapping) защищаются тем, что дублирующиеся wsu:Id отвергаются, а каждая ссылка обязана покрывать eb:Messaging, soap:Body, каждое вложение и присутствующий wsu:Timestamp. От повторного проигрывания спасает проверка wsu:Timestamp по правилам WSS4J плюс дедупликация по messageId. Текст ошибок расшифровки и проверки подписи наружу не уходит: детали только в лог, никакого оракула. Пароли помечены [Sensitive] и вырезаются из логов и дашборда.
Профиль 1.16: что принимаем, что отвергаем
Коннектор реализует профиль eDelivery AS4 1.16 и ничего сверх него. Это не самоограничение, а причина, по которой он разговаривает с реальными партнёрами: там, где реализация начинает «догадываться» о расширениях, интероп ломается.
| Что | Принимаем | Иначе |
|---|---|---|
| Подпись | RSA-SHA256 | на старте конфигурация отвергается; во входящем EBMS:0101 |
| Дайджест | SHA-256 | там же |
| Шифрование payload | AES-128-GCM | на старте; во входящем EBMS:0102 |
| Транспорт ключа | RSA-OAEP, MGF1-SHA256 | на старте; во входящем EBMS:0102 |
| Канонизация | Exclusive C14N | EBMS:0101 |
| Ссылка на ключ | BST / IssuerSerial / KeyIdentifier (на приёме) | на отправке по умолчанию BST |
| Неподписанное или незашифрованное сообщение | никогда | EBMS:0103 |
| Payload в теле SOAP | никогда (только вложения) | EBMS:0002 |
Часть того, что спецификация называет опциональным, просто не входит в скоуп: Pull и асинхронные квитанции. Профиль common 1.16 их и не требует: там явно записано, что синхронная квитанция обязательна, а асинхронная использоваться не должна.
Интероп доказан, а не заявлен
AS4-коннектор, который разговаривает только сам с собой, не доказывает ничего. Этот проверен против независимых реализаций, без строчки общего кода, в обе стороны.
Против Holodeck B2B 8.1.1 проверили обе стороны, по всей матрице профиля: подпись, шифрование, сжатие, каждый из трёх способов ссылки на ключ. Против Apache Domibus 5.1 (эталонный узел EU eDelivery, стенд Harmony AP 2.6.2) тоже в обе стороны: мы шлём, Domibus принимает (RECEIVED); Domibus шлёт, маршрут получает документ, а Domibus принимает нашу квитанцию (ACKNOWLEDGED).
Интероп вскрыл и неочевидные вещи, которые видны только с живым партнёром. Политика eDeliveryAS4Policy Domibus (Strict, без IncludeTimestamp) отвергает wsu:Timestamp, поэтому свою метку коннектор по умолчанию не ставит, а если политика партнёра её просит, включает. А на повтор Domibus отдаёт сохранённую квитанцию первой передачи: если бы мы собирали сообщение заново с новой подписью, её дайджесты не сошлись бы. Поэтому повтор шлёт те же байты. Это ровно тот класс проблем, которого нет в тексте спецификации и который находится только интеропом.
Чем это отличается от Java-MSH и отдельного шлюза
AS4 в .NET-проекте закрывали двумя способами: отдельный Java-MSH или коммерческий шлюз. Разница не в том, умеют они или нет: протокол один. Разница в том, где живёт AS4 относительно вашей логики.
| Коммерческий шлюз | Java MSH (Domibus/Holodeck) | redb.Route.As4 | |
|---|---|---|---|
| Процесс | отдельная коробка | отдельный JVM рядом | ваш .NET-процесс |
| Принятый документ | в inbox-каталоге | в inbox-каталоге | сообщение в маршруте |
| Как забрать | джоба-подборщик | джоба-подборщик | сразу в pipeline |
| Деплой | своя инсталляция | своя инсталляция | вместе с приложением |
| Наблюдаемость | своя панель | логи JVM | общие трейсы и статистика |
| Дальше по обработке | вне шлюза, руками | вне сервера, руками | те же EIP в том же маршруте |
| Лицензия | платная | открытая | открытая, без ключа |
Отдельный MSH оправдан, когда AS4-контур сознательно вынесен в изолированную зону, например в DMZ под управлением другой команды. Когда же документ всё равно уходит в ваш .NET-бэкенд на обработку, промежуточный процесс это лишний прыжок, лишний каталог и лишний компонент в схеме аудита.
Где это применяется
AS4 нужен там, где документ несёт обязательство, а канал диктуете не вы. Несколько типичных ситуаций.
Peppol и e-invoicing. Электронный счёт в сети Peppol ходит по AS4 между access points. Строите свой access point или принимаете счета напрямую: AS4 становится шагом маршрута, а не внешней коробкой. Счёт пришёл, распарсился, провалидировался по UBL/CII, лёг в ERP.
Госсектор ЕС и правовые документы (e-CODEX, e-Justice). Судебные и правовые документы между национальными системами идут по eDelivery AS4 с жёсткими требованиями к подписи и неотрекаемости. Здесь юридический вес документа и есть смысл канала.
Энергетика (ENTSOG). Заявки, план-графики, балансовые документы между газовыми операторами. Обмен обязательный, профиль строгий, партнёры это узлы других операторов.
Транспорт и таможня (eFTI). Электронные транспортные накладные и таможенные документы. Тот же профиль: подпись, шифрование, квитанция.
Корпоративный обмен с госзаказчиком в ЕС. Тендерная, отчётная и регуляторная отчётность, которую европейские заказчики и регуляторы требуют отдавать именно по eDelivery AS4.
Общее во всех случаях одно: документ должен дойти гарантированно и с доказательством, а дальше его надо обработать в вашей системе. Ровно этот стык коннектор и закрывает.
Честные границы
Ничего не бывает бесплатно, и лучше сказать прямо, чего пока нет.
- Pull (получатель сам забирает сообщение) и асинхронные квитанции не реализованы. В профиле common 1.16 они не требуются, но если конкретный партнёр требует Pull, это пока не сюда.
- eDelivery AS4 2.0 (эллиптические кривые вместо RSA-OAEP) не поддержан. Коннектор нацелен на линейку 1.x, common profile 1.16.
- Динамическое обнаружение партнёров (Peppol SMP/SML) это отдельная задача системного уровня. Коннектор берёт адрес и сертификат из соглашения, а откуда их взять, решает ваша интеграция.
- TLS с реальным MSH партнёра вживую не гоняли: оба интероп-стенда ходили по обычному HTTP, а TLS и mutual TLS проверены рукопожатиями на loopback. С живым партнёром это стоит проверить отдельно.
- Повтор движка живёт в памяти, так что для расписаний на часы нужен outbox (это уже не про коннектор, а про паттерн маршрута).
FAQ
Нужен ли рядом Java-MSH? Нет. Это нативный .NET-коннектор: HttpClient, общий Kestrel и System.Security.Cryptography. Он и отправитель, и узел приёма.
AS4 или AS2? Зависит от того, что требует партнёр. AS2 это S/MIME и MDN, AS4 это SOAP, WS-Security и ebMS-квитанция. Европейский госсектор и Peppol диктуют AS4, американская розница и логистика часто AS2. Про AS2 мы рассказывали отдельно; оба коннектора живут в одном процессе и работают рядом.
Где хранятся сертификаты? В As4ConnectionFactory и As4Partner, как X509Certificate2. Пароли помечены [Sensitive], в URI секретов нет.
Можно ли в одном воркере держать HTTP, SOAP, AS2 и AS4? Да. Приём у всех на общем Kestrel-хосте, каждый маршрут слушает свой путь.
Что если партнёр ответил ошибкой? Продюсер бросает As4ErrorSignalException с кодом и описанием; OnException решает, повторять или уводить в dead letter.
Синхронная или асинхронная квитанция? Синхронная, в том же HTTP-ответе. Это требование профиля 1.16, поэтому transacted для отправки запрещён: отложить синхронную квитанцию в транзакцию нельзя.
Установка
dotnet add package redb.Route.As4
Пакет redb.Route.As4 на NuGet, исходники и полный справочник по DSL в README коннектора. AS4 это ещё один транспорт в семействе redb.Route, рядом с Kafka, RabbitMQ, AS2, IBM MQ и остальными: тот же From → … → To, те же EIP, та же наблюдаемость. Разница только в том, что передаётся SOAP-конверт с ebMS-заголовками, которого ждёт европейский узел обмена.
Если было полезно, ⭐ на GitHub поможет другим это найти.
Другие мои статьи: redb.ru/articles, ещё на Хабре.