Hlavní navigace

Komunikace v distribuovaných systémech: digitální podpisy zpráv

31. 3. 2021
Doba čtení: 6 minut

Sdílet

 Autor: Depositphotos
Představte si, že se připojujete do sítě, ve které vystupují desítky či stovky uzlů provozovaných různými organizacemi. Pokud od nějakého uzlu přijmete zprávu, jak si můžete být jisti, že je to zpráva skutečně od něho?

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.

Hacking tip

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

Autor článku

Jiří Raška pracuje na pozici IT architekta. Poslední roky se zaměřuje na integrační a komunikační projekty ve zdravotnictví. Mezi jeho koníčky patří také paragliding a jízda na horském kole.