This is, in fact, mostly already the standard way to use double-submit cookies in script-driven apps. The main innovation is signing the anti-CSRF token, which actually doesn't add anything in most cases.
Now, for the actual security of your design. The most obvious potential flaw that I see is that, while you don't say anything about how the anti-CSRF token is generated, it's not obviously tied to the session. Using double-submit cookie or any other secret-token-based CSRF prevention is much weaker if the token is not tied to the session. For example, in your scheme,
"Attacker cannot, by any means, change the contents of the CSRF cookie because it's signed"
is unfortunately false (or at least, not enough information is given to prove it); an attacker who has the ability to edit/plant cookies can simply log into the site themselves, retrieve their own signed anti-CSRF token, plant that in the victim's browser (overwriting any that they currently have), and then submit requests with that token in the request (assuming not blocked by SOP or similar).
Requiring that the attacker have the ability to edit/plant cookies does raise the bar significantly (especially if you use HSTS, and one of the cookie name prefixes that explicitly exist to combat cookie-planting attacks), but fundamentally, the signing isn't doing anything that using a double-submit cookie doesn't already do; if the token isn't tied to the session then an attacker can (one must assume) obtain their own valid anti-CSRF token on demand whether or not it's signed, and if it is tied to the session then you don't need the cookie at all - just stash it in LocalStorage or similar - and the signing doesn't matter because there's presumably only one valid value for that session.
Once you have tied the anti-CSRF token to the session (or at least the user), it's fine to use a double-submit cookie to avoid storing (and looking up) the token on the server. However, there's a potentially more-elegant option at the bottom of this answer.
I do want to mention that this is technically overkill. While there's nothing inherently wrong with using stronger security than is needed, your implied site design already has two different anti-CSRF protections built into it, and adding a third is probably not necessary and might not be worth the increased complexity and attack surface. The two in question are:
- Adding any kind of secret via script rather than via cookie is inherently CSRF-proof, because the attacker doesn't know the secret. The secret can be something that is also sent in a cookie - this is how double-submit cookie pattern works normally, whether the anti-CSRF token is injected by script or present in a
<input type="hidden">
form element - but it could also be stored server-side (requires state, but you loosely implied that you store session tokens statefully anyhow), a signed value such as a JWT (typically stateless, but can be tied to a stateful value such as the session token), or even just a hash (not really any need to make it a keyed hash though you can if you want) of another secret such as the session token. The only requirements are that the secret is not predictable and is tied to the specific user (and ideally to their specific session).
- Adding any kind of header - or indeed requiring the use of most content types at the server - is inherently proof against CSRF, unless you deeply misconfigure CORS. By default, cross-origin requests that use any custom headers, or in general set any header to any but a small number of allowed values, are disallowed. CORS can be used to allow them on an origin-by-origin basis, and obviously you should not allow origins that are not, in fact, your own sites on which this app is supposed to run, or at least similarly trusted. Unless you go out of your way to thus misconfigure CORS - which will also severely impact site security in other ways - you generally just don't need to worry about it. Requiring (meaning you enforce its presence at the server) a custom header of
CSRF: no
is perfectly valid CSRF protection, even though it contains no secrets at all, because "cross-origin requests can't set unapproved headers" is part of the same-origin policy (SOP) that is fundamental to webapp security.
If you do want to go with an anti-CSRF-token type of approach, may I suggest a more elegant one? You already have a secret value that the server either knows or can verify statelessly (the session token). Simply hash it with any secure hash function such as a member of the SHA2 or SHA3 families (you can use an HMAC or other keyed hash if you want, but there's no need; if the attacker has access to the victim's session token to generate the hash then it's game over anyhow), send that hashed value to the client (e.g. in an HTTPS response) such that the client stores it in a variable/local storage, and then on every state-changing request from the client, re-hash the session token (after verifying that it's valid) to verify the validity of the presented anti-CSRF token (which may have been transmitted in a header, or the body).
Note that this approach does require that the session token not change unexpectedly. With a random opaque token this is usually true - the token changes only in response to events that would be a normal time for an anti-CSRF token to also change, such as logging in - but stateless tokens (such as the typical use of JWTs) usually change much more often (as protection against misusing a compromised or outdated token, given the inherent difficulty of revoking a stateless token). If your site uses an automatically-rotating session token, you should base the anti-CSRF token on a part that does not change (this could be a random string of sufficient entropy inserted into an arbitrary claim in a JWT, a non-rotating refresh token although ideally those also rotate, or similar).