Ve stejné pozici jste, pokud požádáte jiný uzel o službu. Jak si můžete být jisti, že vám odpověděl právě tento uzel? To jsou velice důležité otázky, pokud operujete v síti strojů, které nejsou ve správě jednoho subjektu.
V tomto článku vám neodpovím na všechny otázky s tímto fenoménem spojených. Podívám se na jeden dílčí problém, a sice jak si ověřit, od koho jsem zprávu dostal.
Jednou z možností jsou digitální podpisy každé právy pomocí dvojice asymetrických klíčů.
Dříve, než odesílající uzel předá zprávu (ať již je to požadavek nebo odpověď) do message brokeru, vytvoří digitální podpis obsahu předávané zprávy s pomocí svého privátního klíče. Podpis je připojen ke zprávě jako její hlavička.
Příjemce podpis zprávy ověří pomocí veřejného klíče odesilatele a porovná s obsahem zprávy.
Pochopitelně je zde otázka, jak příjemce důvěryhodně získá veřejný klíč odesilatele. Odpověď na tuto otázku si ponechám jako námět na některý z dalších článků.
Příklady k tomuto článku je možné najít v package: example08
Uložiště klíčů
Pro prezentaci základní funkcionality podpisu a ověření zpráv v Camel vystačím s jednoduchým úložištěm typu mapa. Klíčem v úložišti bude název uzlu, hodnotou pak dvojice asymetrických klíčů vygenerovaných při spuštění aplikace.
Úložiště mám vytvoření v rámci třídy s metodou main:
private static final String[] NODES = {"applicant01", "provider01", "provider02", "provider03"}; @Bean public Map<String, KeyPair> keys() { Map<String, KeyPair> m = new HashMap<>(); try { KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA"); kpg.initialize(2048); for (String name : NODES) { m.put(name, kpg.generateKeyPair()); } } catch (NoSuchAlgorithmException e) { logger.error("Security exception", e); } logger.info("Key Pairs Initialized ..."); return m; }
Předávané zprávy
Všechny vydefinované typy zpráv jsou Java Bean, jejíž definice jsou v package entity.
Definované Camel cesty
Takto vypadají všechny definované cesty v Camel:
@Component public class CamelRoutes extends RouteBuilder { private static final Logger logger = LoggerFactory.getLogger(CamelRoutes.class); private static final String HEADER_CLASS_NAME = "ObjectClassName"; private static final String HEADER_SIGNATURE = "ObjectDigitalSignature"; private static final String HEADER_NODE_NAME = "NodeName"; @Autowired private ObjectMapper jsonMapper; @Autowired Map<String, KeyPair> keys; @Autowired private ProducerTemplate producerTemplate; @Override public void configure() { // Signing Route definitions ... from("direct:object-sign").routeId("object-sign") .process(exchange -> { KeyPair keyPair = keys.get(exchange.getMessage().getHeader(HEADER_NODE_NAME, String.class)); if (keyPair != null) exchange.getMessage().setHeader(DigitalSignatureConstants.SIGNATURE_PRIVATE_KEY, keyPair.getPrivate()); logger.info("SIGN OBJECT"); }) .to("crypto:sign://basic") .process(exchange -> exchange.getMessage().setHeader(HEADER_SIGNATURE, exchange.getMessage().getHeader(DigitalSignatureConstants.SIGNATURE, String.class))); from("direct:object-verify").routeId("object-verify") .process(exchange -> { exchange.getMessage().setHeader(DigitalSignatureConstants.SIGNATURE, exchange.getMessage().getHeader(HEADER_SIGNATURE, String.class)); KeyPair keyPair = keys.get(exchange.getMessage().getHeader(HEADER_NODE_NAME, String.class)); if (keyPair != null) exchange.getMessage().setHeader(DigitalSignatureConstants.SIGNATURE_PUBLIC_KEY_OR_CERT, keyPair.getPublic()); logger.info("VERIFY OBJECT"); }) .to("crypto:verify://basic"); // Serialization Route definitions ... from("direct:object-mapping").routeId("object-mapping") .process(exchange -> { Token token = exchange.getMessage().getBody(Token.class); exchange.getMessage().setHeader(HEADER_CLASS_NAME, token.getClass().getCanonicalName()); exchange.getMessage().setBody(jsonMapper.writeValueAsString(exchange.getMessage().getBody()), String.class); }); from("direct:reverse-mapping").routeId("reverse-mapping") .process(exchange -> { String className = exchange.getMessage().getHeader(HEADER_CLASS_NAME, String.class); Token token = (Token) jsonMapper.readValue(exchange.getMessage().getBody(String.class), Class.forName(className)); exchange.getMessage().setBody(token); }); // Applicant Route definitions ... from("direct:applicant01").routeId("applicant01") .process(exchange -> exchange.getMessage().setHeader(HEADER_NODE_NAME, exchange.getMessage().getBody(Token.class).getName())) .to("direct:object-mapping") .to("direct:object-sign") .multicast() .aggregationStrategy((oldExchange, newExchange) -> { Exchange result; producerTemplate.send("direct:object-verify", newExchange); producerTemplate.send("direct:reverse-mapping", newExchange); List<Response> list; if (oldExchange != null) { list = oldExchange.getIn().getBody(List.class); result = oldExchange; } else { list = new ArrayList<>(); result = newExchange; } Response resp = newExchange.getMessage().getBody(Response.class); list.add(newExchange.getMessage().getBody(Response.class)); result.getMessage().setBody(list, List.class); return result; }) .to("activemq:queue:QUEUE-1", "activemq:queue:QUEUE-2", "activemq:queue:QUEUE-3") .end(); // Provider Route definitions ... from("activemq:queue:QUEUE-1").routeId("provider01") .to("direct:object-verify") .to("direct:reverse-mapping") .process(exchange -> { Request request = exchange.getMessage().getBody(Request.class); Response response = new Response("provider01", new Date(), request.getValue() + 10); exchange.getMessage().setBody(response); }) .process(exchange -> exchange.getMessage().setHeader(HEADER_NODE_NAME, exchange.getMessage().getBody(Token.class).getName())) .to("direct:object-mapping") .to("direct:object-sign"); from("activemq:queue:QUEUE-2").routeId("provider02") .to("direct:object-verify") .to("direct:reverse-mapping") .process(exchange -> { Request request = exchange.getMessage().getBody(Request.class); Response response = new Response("provider02", new Date(), (request.getValue() + 10) * 2); exchange.getMessage().setBody(response); }) .process(exchange -> exchange.getMessage().setHeader(HEADER_NODE_NAME, exchange.getMessage().getBody(Token.class).getName())) .to("direct:object-mapping") .to("direct:object-sign"); from("activemq:queue:QUEUE-3").routeId("provider03") .to("direct:object-verify") .to("direct:reverse-mapping") .process(exchange -> { Request request = exchange.getMessage().getBody(Request.class); Response response = new Response("provider03", new Date(), (request.getValue() + 50) * request.getValue()); exchange.getMessage().setBody(response); }) .process(exchange -> exchange.getMessage().setHeader(HEADER_NODE_NAME, exchange.getMessage().getBody(Token.class).getName())) .to("direct:object-mapping") .to("direct:object-sign"); } }
Opět úzce navazuji na dřívější řešení pro serializaci zpráv.
Pro podporu digitálních podpisů a jejich ověření mně přibyly cesty:
- object-sign – zjistí z hlavičky název uzlu (hlavička NodeName) a vyhledá k němu odpovídající privátní klíč v úložišti. Následně vytvoří podpis zprávy a uloží jej do hlavičky ObjectDigitalSignature.
- object-verify – zjistí z hlavičky název uzlu (hlavička NodeName) a vyhledá k němu odpovídající veřejný klíč v úložišti. Následně ověří, že podpis předaný v hlavičce ObjectDigitalSignature odpovídá obsahu zprávy.
Volání cesty pro digitální podepsání následuje ihned po provedení serializace objektu. Stejně tak volání cesty pro ověření podpisu předchází volání deserializace objektu.
Vzhledem k tomu, že serializace i podepisování se může lišit při komunikaci s každým poskytovatelem služeb, mám opět vytvořenu vlastní strategii pro spojování výsledků.
REST API aplikace
V tomto případě se jedná o jednoduchou službu. Nic zvláštního na ní vidět není:
@RestController public class ServiceController { @Autowired private ProducerTemplate producerTemplate; @RequestMapping(value = "/rest/appl01") public ResponseEntity<List<Response>> restApplicant01(@RequestBody Request request) { if (request.getName() == null) request.setName("rest-applicant01"); request.setTs(new Date()); Map<String, Object> headers = new HashMap<>(); List<Response> response = producerTemplate.requestBodyAndHeaders("direct:applicant01", request, headers, List.class); if (response != null) { return ResponseEntity.ok(response); } else { return ResponseEntity.notFound().build(); } } }
Vzhledem k tomu, že chci prezentovat pouze základní funkcionalitu podpisu a jejich ověření bez širšího experimentování, vystačím s jednoduchou podobou volání jedné služby.
Jak si to vyzkoušet
Takto to vypadá, pokud zavolám službu:
[raska@localhost ~]$ curl -s -d '{ "value": "1234", "name": "applicant01" }' -H 'Content-Type: application/json' 'http://localhost:8080/rest/appl01' | jq . [ { "name": "provider01", "ts": "2021-01-03T09:55:10.950+00:00", "result": 1244 }, { "name": "provider02", "ts": "2021-01-03T09:55:11.068+00:00", "result": 2488 }, { "name": "provider03", "ts": "2021-01-03T09:55:11.158+00:00", "result": 1584456 } ]
Dostal jsem odpovědi od všech poskytovatelů služeb. Toto bych měl vidět v logu jako potvrzení, že byly vytvořeny a následně ověřeny podpisy každé zprávy:
2021-01-03 10:55:10.844 INFO [tp1740095856-20]: SIGN OBJECT 2021-01-03 10:55:10.933 INFO [nsumer[QUEUE-1]]: VERIFY OBJECT 2021-01-03 10:55:10.955 INFO [nsumer[QUEUE-1]]: SIGN OBJECT 2021-01-03 10:55:11.006 INFO [anager[QUEUE-1]]: VERIFY OBJECT 2021-01-03 10:55:11.063 INFO [nsumer[QUEUE-2]]: VERIFY OBJECT 2021-01-03 10:55:11.068 INFO [nsumer[QUEUE-2]]: SIGN OBJECT 2021-01-03 10:55:11.136 INFO [anager[QUEUE-2]]: VERIFY OBJECT 2021-01-03 10:55:11.156 INFO [nsumer[QUEUE-3]]: VERIFY OBJECT 2021-01-03 10:55:11.162 INFO [nsumer[QUEUE-3]]: SIGN OBJECT 2021-01-03 10:55:11.226 INFO [anager[QUEUE-3]]: VERIFY OBJECT