Support JWS with multiple signatures

Issue #67 on hold
Richard Körber created an issue

First of all, thank you for jose4j. Without it, acme4j would not exist.

A coming feature of the ACME protocol requires a body that is signed with two JWS signatures. I had a look at the jose4j API, but it does not seem to support multiple signatures. Can you add it, or is there a way to do multiple signatures with the existing API?

Comments (9)

  1. Brian Campbell repo owner

    Hi Richard, that's cool about acme4j - it's always nice to hear about usages of my little project.

    There is no direct support for multiple signatures in the jose4j API at present. Supporting JOSE's compact serializations, which only have one signature, has always been the main scope of this project. But I suspect that, with a little work, the existing API can accommodate it. Maybe. Probably.

    Can you give me some more detail on what ACME is doing with two JWS signatures? I've listened in on an ACME meeting or two but haven't followed it in any detail. So I'm vaguely familiar with the concept but have basically zero knowledge of the details. Is the JWS JSON Serialization (https://tools.ietf.org/html/rfc7515#section-7.2.1) being used? Or something else?

  2. Brian Campbell repo owner

    Below is some code that shows, using the general JWS JSON serialization, how the existing APIs might be used to piece together support for two JWS signatures over the same content. This works largely due to the similarities in how the signature is computed for both the compact and JSON serialization in JWS, which JWS describes like this:

       Each JWS Signature value is computed using the parameters of the
       corresponding JOSE Header value in the same manner as for the JWS
       Compact Serialization.  This has the desirable property that each JWS
       Signature value represented in the "signatures" array is identical to
       the value that would have been computed for the same parameter in the
       JWS Compact Serialization, provided that the JWS Protected Header
       value for that signature/MAC computation (which represents the
       integrity-protected Header Parameter values) matches that used in the
       JWS Compact Serialization.
    

    Here's the code that shows how it might be possible to produce and consume two JWS signatures on the same body/payload content.

            // The keys and content are borrowed from the  JOSE Cookbook
            // https://tools.ietf.org/html/rfc7520
    
            // EC JWK from https://tools.ietf.org/html/rfc7520#section-3.2
            PublicJsonWebKey ecFig2Jwk = PublicJsonWebKey.Factory.newPublicJwk("{\n" +
                    "     \"kty\": \"EC\",\n" +
                    "     \"kid\": \"bilbo.baggins@hobbiton.example\",\n" +
                    "     \"use\": \"sig\",\n" +
                    "     \"crv\": \"P-521\",\n" +
                    "     \"x\": \"AHKZLLOsCOzz5cY97ewNUajB957y-C-U88c3v13nmGZx6sYl_oJXu9\n" +
                    "         A5RkTKqjqvjyekWF-7ytDyRXYgCF5cj0Kt\",\n" +
                    "     \"y\": \"AdymlHvOiLxXkEhayXQnNCvDX4h9htZaCJN34kfmC6pV5OhQHiraVy\n" +
                    "         SsUdaQkAgDPrwQrJmbnX9cwlGfP-HqHZR1\",\n" +
                    "     \"d\": \"AAhRON2r9cqXX1hg-RoI6R1tX5p2rUAYdmpHZoC1XNM56KtscrX6zb\n" +
                    "         KipQrCW9CGZH3T4ubpnoTKLDYJ_fF3_rJt\"\n" +
                    "   }\n");
    
            // RSA JWK from https://tools.ietf.org/html/rfc7520#section-3.4
            PublicJsonWebKey rsaFig4Jwk = PublicJsonWebKey.Factory.newPublicJwk("{\n" +
                    "     \"kty\": \"RSA\",\n" +
                    "     \"kid\": \"bilbo.baggins@hobbiton.example\",\n" +
                    "     \"use\": \"sig\",\n" +
                    "     \"n\": \"n4EPtAOCc9AlkeQHPzHStgAbgs7bTZLwUBZdR8_KuKPEHLd4rHVTeT\n" +
                    "         -O-XV2jRojdNhxJWTDvNd7nqQ0VEiZQHz_AJmSCpMaJMRBSFKrKb2wqV\n" +
                    "         wGU_NsYOYL-QtiWN2lbzcEe6XC0dApr5ydQLrHqkHHig3RBordaZ6Aj-\n" +
                    "         oBHqFEHYpPe7Tpe-OfVfHd1E6cS6M1FZcD1NNLYD5lFHpPI9bTwJlsde\n" +
                    "         3uhGqC0ZCuEHg8lhzwOHrtIQbS0FVbb9k3-tVTU4fg_3L_vniUFAKwuC\n" +
                    "         LqKnS2BYwdq_mzSnbLY7h_qixoR7jig3__kRhuaxwUkRz5iaiQkqgc5g\n" +
                    "         HdrNP5zw\",\n" +
                    "     \"e\": \"AQAB\",\n" +
                    "     \"d\": \"bWUC9B-EFRIo8kpGfh0ZuyGPvMNKvYWNtB_ikiH9k20eT-O1q_I78e\n" +
                    "         iZkpXxXQ0UTEs2LsNRS-8uJbvQ-A1irkwMSMkK1J3XTGgdrhCku9gRld\n" +
                    "         Y7sNA_AKZGh-Q661_42rINLRCe8W-nZ34ui_qOfkLnK9QWDDqpaIsA-b\n" +
                    "         MwWWSDFu2MUBYwkHTMEzLYGqOe04noqeq1hExBTHBOBdkMXiuFhUq1BU\n" +
                    "         6l-DqEiWxqg82sXt2h-LMnT3046AOYJoRioz75tSUQfGCshWTBnP5uDj\n" +
                    "         d18kKhyv07lhfSJdrPdM5Plyl21hsFf4L_mHCuoFau7gdsPfHPxxjVOc\n" +
                    "         OpBrQzwQ\",\n" +
                    "     \"p\": \"3Slxg_DwTXJcb6095RoXygQCAZ5RnAvZlno1yhHtnUex_fp7AZ_9nR\n" +
                    "         aO7HX_-SFfGQeutao2TDjDAWU4Vupk8rw9JR0AzZ0N2fvuIAmr_WCsmG\n" +
                    "         peNqQnev1T7IyEsnh8UMt-n5CafhkikzhEsrmndH6LxOrvRJlsPp6Zv8\n" +
                    "         bUq0k\",\n" +
                    "     \"q\": \"uKE2dh-cTf6ERF4k4e_jy78GfPYUIaUyoSSJuBzp3Cubk3OCqs6grT\n" +
                    "         8bR_cu0Dm1MZwWmtdqDyI95HrUeq3MP15vMMON8lHTeZu2lmKvwqW7an\n" +
                    "         V5UzhM1iZ7z4yMkuUwFWoBvyY898EXvRD-hdqRxHlSqAZ192zB3pVFJ0\n" +
                    "         s7pFc\",\n" +
                    "     \"dp\": \"B8PVvXkvJrj2L-GYQ7v3y9r6Kw5g9SahXBwsWUzp19TVlgI-YV85q\n" +
                    "         1NIb1rxQtD-IsXXR3-TanevuRPRt5OBOdiMGQp8pbt26gljYfKU_E9xn\n" +
                    "         -RULHz0-ed9E9gXLKD4VGngpz-PfQ_q29pk5xWHoJp009Qf1HvChixRX\n" +
                    "         59ehik\",\n" +
                    "     \"dq\": \"CLDmDGduhylc9o7r84rEUVn7pzQ6PF83Y-iBZx5NT-TpnOZKF1pEr\n" +
                    "         AMVeKzFEl41DlHHqqBLSM0W1sOFbwTxYWZDm6sI6og5iTbwQGIC3gnJK\n" +
                    "         bi_7k_vJgGHwHxgPaX2PnvP-zyEkDERuf-ry4c_Z11Cq9AqC2yeL6kdK\n" +
                    "         T1cYF8\",\n" +
                    "     \"qi\": \"3PiqvXQN0zwMeE-sBvZgi289XP9XCQF3VWqPzMKnIgQp7_Tugo6-N\n" +
                    "         ZBKCQsMf3HaEGBjTVJs_jcK8-TRXvaKe-7ZMaQj8VfBdYkssbu0NKDDh\n" +
                    "         jJ-GtiseaDVWt7dcH0cfwxgFUHpQh7FoCrjFJ6h6ZEpMF6xmujs4qMpP\n" +
                    "         z8aaI4\"\n" +
                    "   }\n");
    
            //  The payload (body content to be signed) from https://tools.ietf.org/html/rfc7520#section-4
            String rawPayload = "It’s a dangerous business, Frodo, going out your door. " +
                    "You step onto the road, and if you don't keep your feet, there’s no" +
                    " knowing where you might be swept off to.";
    
            // to sign w/ multiple signatures:
    
            // sign the payload with the RSA key to get one of the signatures
            JsonWebSignature firstJws = new JsonWebSignature();
            firstJws.setPayload(rawPayload);
            firstJws.setAlgorithmHeaderValue(AlgorithmIdentifiers.RSA_USING_SHA256);
            firstJws.setKeyIdHeaderValue(rsaFig4Jwk.getKeyId());
            firstJws.setKey(rsaFig4Jwk.getPrivateKey());
            firstJws.sign();
    
            // Get the encoded payload, the thing that's signed, that will
            // put into the top level of the General JWS JSON Serialization
            // https://tools.ietf.org/html/rfc7515#section-7.2.1
            String encodedPayload = firstJws.getEncodedPayload();
    
            // Get the encoded header and payload, which will be used to assemble one
            // of the two signatures in the General JWS JSON Serialization
            String encodedHeader1 = firstJws.getHeaders().getEncodedHeader();
            String encodedSignature1 = firstJws.getEncodedSignature();
    
            // sign the payload with the EC key to get the other signature
            JsonWebSignature secondJws = new JsonWebSignature();
            secondJws.setEncodedPayload(encodedPayload);
            secondJws.setAlgorithmHeaderValue(AlgorithmIdentifiers.ECDSA_USING_P521_CURVE_AND_SHA512);
            secondJws.setKeyIdHeaderValue(ecFig2Jwk.getKeyId());
            secondJws.setKey(ecFig2Jwk.getPrivateKey());
            secondJws.sign();
    
            // Get the encoded header and payload, which will be used to assemble the
            // one of the two signatures in the General JWS JSON Serialization
            String encodedHeader2 = secondJws.getHeaders().getEncodedHeader();
            String encodedSignature2 = secondJws.getEncodedSignature();
    
            // jose4j uses a very simple JSON processor that operates with
            // basic types and maps and lists. Here we assemble a data structure, which
            // that JSON processor will turn into JSON that conforms to the
            // General JWS JSON Serialization.
            // You could use a different JSON lib here too, should you want.
            Map<String, Object> topLevel = new HashMap<>();
            topLevel.put("payload", encodedPayload);
            List<Map<String, Object>> signatures = new ArrayList<>();
            Map<String, Object> firstSignature = new HashMap<>();
            firstSignature.put("protected", encodedHeader1);
            firstSignature.put("signature", encodedSignature1);
            signatures.add(firstSignature);
            Map<String, Object> secondSignature = new HashMap<>();
            secondSignature.put("protected", encodedHeader2);
            secondSignature.put("signature", encodedSignature2);
            signatures.add(secondSignature);
            topLevel.put("signatures", signatures);
    
            // produce the General JWS JSON Serialization
            String generalJwsJsonSerialization = JsonUtil.toJson(topLevel);
            System.out.println("General JWS JSON Serialization with multiple" +
                    " signatures over the same content: " +generalJwsJsonSerialization);
    
    
    
            // To verify:
    
            // Parse the JSON
            Map<String, Object> parsed = JsonUtil.parseJson(generalJwsJsonSerialization);
    
            // Grab the encoded payload from the top level
            encodedPayload = (String) parsed.get("payload");
    
            // get the array of signatures
            signatures = (List<Map<String, Object>>)parsed.get("signatures");
    
            // get the encoded header (named protected) and signature value from the first signature
            firstSignature = signatures.get(0);
            encodedHeader1 = (String)firstSignature.get("protected");
            encodedSignature1 = (String)firstSignature.get("signature");
    
            // Assemble a JWS Compact Serialization https://tools.ietf.org/html/rfc7515#section-7.1
            // for the first signature from the parts so it can be verified with jose4j's JsonWebSignature
            String serialized = CompactSerializer.serialize(encodedHeader1, encodedPayload, encodedSignature1);
            JsonWebSignature jws = new JsonWebSignature();
            jws.setCompactSerialization(serialized);
            jws.setKey(rsaFig4Jwk.getPublicKey());
            System.out.println("First signature verify: " + jws.verifySignature());
    
            // get the encoded header and signature value from the second signature
            secondSignature = signatures.get(1);
            encodedHeader2 = (String)secondSignature.get("protected");
            encodedSignature2 = (String)secondSignature.get("signature");
    
            // Assemble a JWS Compact Serialization https://tools.ietf.org/html/rfc7515#section-7.1
            // for the first signature from the parts so it can be verified with jose4j's JsonWebSignature
            jws = new JsonWebSignature();
            serialized = CompactSerializer.serialize(encodedHeader2, encodedPayload, encodedSignature2);
            jws.setCompactSerialization(serialized);
            jws.setKey(ecFig2Jwk.getPublicKey());
            System.out.println("Second signature verify: " + jws.verifySignature());
    
            System.out.println("The (twice) signed payload: " + jws.getPayload());
    
  3. Richard Körber reporter

    Thank you for your answer and your code example, Brian! I don't know too much about JWS (and JOSE), so I wasn't aware that compact serialization only supports one signature.

    The current ACME proposal thinks about using two signatures for a key change command. The command will be signed with both the old key (the one to be replaced) and the new key. That's already the third concept about the key change API, so I think I should wait for a while and see if this is the final idea now. 😄

    I agree that this is out of jose4j's scope, and as long as there is a solution like your example, it's fine for me...

  4. Brian Campbell repo owner

    The code in the previous comment could be used (with a few tweaks) to produce the message described in https://tools.ietf.org/html/draft-ietf-acme-acme-03#section-6.2.1

    I am in the ACME WG meeting right now at IETF 96, however, it sounds like there's still some debate on how exactly to structure the messages and signatures for key roll-over. So waiting to see how that shakes out is probably a good idea.

  5. Brian Campbell repo owner

    FWIW, the JSON serializations aren't necessarily out of scope for jose4j. But it's not a big priority and, while the above example is pretty simple, designing a decent API to support it is a bigger task that I'm not likely to get to in the near future.

  6. Richard Körber reporter

    Your code example seemed to work. I had implemented the key-change request like that, and got a "too many signatures" error from the server. It was because of an error in the ACME documentation. The actual key-change request only needs a single signature again.

    This bug can be closed, unless you need it as a reminder for adding multiple signature support in jose4j.

    Brian, thank you for your help! Even though I eventually didn't need your code example, I have learned a lot about JOSE and jose4j.

  7. Brian Campbell repo owner

    You are welcome and I'm glad it's been helpful even if not actually used.

    I'm going to mark this one as "on hold" for the time being. No plans to add JWS JSON serialization w/ with multiple signatures at this time.

  8. Log in to comment