Commit 7635098c authored by jan.koester's avatar jan.koester
Browse files

x509: verify ECDSA P-384 and SHA-384 signatures

Let's Encrypt's ECDSA hierarchy (Root YE, YE1, ...) signs with P-384 keys
and ecdsa-with-SHA384, so chains like auth.docker.io's failed with "no
trusted issuer found". verifySignedBy now handles ecdsa-with-SHA256/384
on P-256 and P-384 issuer keys (the curve from the key's parameters) and
sha384WithRSAEncryption besides sha256WithRSAEncryption.

ecdsa_verify.cpp: P-384 field/order arithmetic (Montgomery, verify only,
public data) and ECDSA verification over a digest for both curves.

Co-Authored-By: default avatarClaude Opus 5.5 <noreply@anthropic.com>
parent 24eced9a
Loading
Loading
Loading
Loading
+4 −0
Original line number Diff line number Diff line
@@ -14,6 +14,8 @@ list(APPEND netplussrc
    crypto/rsa_pss_sha256.cpp
    crypto/rsa_pss_sha384.cpp
    crypto/rsa_pkcs1_sha256.cpp
    crypto/rsa_pkcs1_sha384.cpp
    crypto/ecdsa_verify.cpp
    crypto/curve25519.cpp
    crypto/pkcs12.cpp
    crypto/tls.cpp
@@ -76,6 +78,8 @@ set(headers
    crypto/rsa_pss_sha256.h
    crypto/rsa_pss_sha384.h
    crypto/rsa_pkcs1_sha256.h
    crypto/rsa_pkcs1_sha384.h
    crypto/ecdsa_verify.h
    crypto/sha.h
    crypto/pkcs12.h
    crypto/tls.h
+337 −0
Original line number Diff line number Diff line
#include "crypto/ecdsa_verify.h"

#include <cstring>

#include "crypto/ecc_p256.h"
#include "crypto/ecc_u256.h"

namespace netplus {

namespace {

// ---- DER signature: SEQUENCE { INTEGER r, INTEGER s } ----

bool derLength(const std::vector<uint8_t> &d, size_t &i, size_t &len) {
    if (i >= d.size()) return false;
    uint8_t b = d[i++];
    if (b < 0x80) {
        len = b;
        return true;
    }
    size_t n = b & 0x7f;
    if (n == 0 || n > 2 || i + n > d.size()) return false;
    len = 0;
    while (n--) len = len << 8 | d[i++];
    return true;
}

// an unsigned INTEGER into width big-endian bytes
bool derInteger(const std::vector<uint8_t> &d, size_t &i, uint8_t *out, size_t width) {
    size_t len;
    if (i >= d.size() || d[i++] != 0x02 || !derLength(d, i, len) || len == 0 || i + len > d.size()) return false;
    const uint8_t *p = d.data() + i;
    i += len;
    if (p[0] & 0x80) return false;  // negative
    while (len > 1 && p[0] == 0) {
        ++p;
        --len;
    }
    if (len > width) return false;
    std::memset(out, 0, width);
    std::memcpy(out + width - len, p, len);
    return true;
}

bool decodeSignature(const std::vector<uint8_t> &d, uint8_t *r, uint8_t *s, size_t width) {
    size_t i = 0, len;
    if (d.empty() || d[i++] != 0x30 || !derLength(d, i, len) || i + len != d.size()) return false;
    return derInteger(d, i, r, width) && derInteger(d, i, s, width) && i == d.size();
}

// ---- P-256: the existing field and point code ----

bool verifyP256(const std::vector<uint8_t> &pubRaw, const std::vector<uint8_t> &digest, const std::vector<uint8_t> &sig) {
    P256Point q;
    if (!P256Point::decodeTls(q, pubRaw.data(), pubRaw.size()) || q.inf || !q.isOnCurve()) return false;
    uint8_t rb[32], sb[32], zb[32] = {0};
    if (!decodeSignature(sig, rb, sb, 32)) return false;
    u256 r = u256::fromBigEndian(rb), s = u256::fromBigEndian(sb);
    if (r.isZero() || r >= P256_N || s.isZero() || s >= P256_N) return false;
    // the leftmost 256 bits of the digest
    size_t take = digest.size() < 32 ? digest.size() : 32;
    std::memcpy(zb + 32 - take, digest.data(), take);
    u256 z = u256::fromBigEndian(zb);
    while (z >= P256_N) z = z - P256_N;
    u256 w = s.scalarInverseModN();
    u256 u1 = z.scalarMulModN(w), u2 = r.scalarMulModN(w);
    uint8_t u1b[32], u2b[32];
    u1.toBigEndian(u1b);
    u2.toBigEndian(u2b);
    P256Point R = P256Point::scalarMulBase(u1b) + q.scalarMul(u2b);
    if (R.inf) return false;
    std::vector<uint8_t> enc = R.encodeTls();
    u256 x = u256::fromBigEndian(enc.data() + 1);
    while (x >= P256_N) x = x - P256_N;
    return x == r;
}

// ---- P-384 ----

const int L = 12;  // 32-bit limbs, little endian

struct N384 {
    uint32_t w[L];
};

N384 fromBE(const uint8_t *be) {
    N384 a;
    for (int i = 0; i < L; ++i)
        a.w[i] = (uint32_t)be[47 - 4 * i] | (uint32_t)be[46 - 4 * i] << 8 | (uint32_t)be[45 - 4 * i] << 16 |
                 (uint32_t)be[44 - 4 * i] << 24;
    return a;
}

N384 fromHex(const char *hex) {
    uint8_t be[48];
    for (int i = 0; i < 48; ++i) {
        auto v = [](char c) { return c <= '9' ? c - '0' : (c | 32) - 'a' + 10; };
        be[i] = (uint8_t)(v(hex[2 * i]) << 4 | v(hex[2 * i + 1]));
    }
    return fromBE(be);
}

int cmp(const N384 &a, const N384 &b) {
    for (int i = L - 1; i >= 0; --i)
        if (a.w[i] != b.w[i]) return a.w[i] < b.w[i] ? -1 : 1;
    return 0;
}

bool isZero(const N384 &a) {
    for (int i = 0; i < L; ++i)
        if (a.w[i]) return false;
    return true;
}

uint32_t addTo(N384 &a, const N384 &b) {
    uint64_t c = 0;
    for (int i = 0; i < L; ++i) {
        c += (uint64_t)a.w[i] + b.w[i];
        a.w[i] = (uint32_t)c;
        c >>= 32;
    }
    return (uint32_t)c;
}

uint32_t subFrom(N384 &a, const N384 &b) {
    int64_t c = 0;
    for (int i = 0; i < L; ++i) {
        c += (int64_t)a.w[i] - b.w[i];
        a.w[i] = (uint32_t)c;
        c >>= 32;
    }
    return (uint32_t)(c & 1);
}

// arithmetic modulo m in the Montgomery domain (R = 2^384)
struct Mod {
    N384 m, r2;
    uint32_t n0;  // -m^-1 mod 2^32

    explicit Mod(const N384 &mod) : m(mod) {
        uint32_t inv = 1;
        for (int i = 0; i < 5; ++i) inv *= 2 - m.w[0] * inv;  // Newton: m * inv == 1 mod 2^32
        n0 = (uint32_t)(0 - inv);
        // R^2 mod m: 1 doubled 768 times
        N384 x{};
        x.w[0] = 1;
        for (int i = 0; i < 768; ++i) {
            uint32_t carry = addTo(x, x);
            if (carry || cmp(x, m) >= 0) subFrom(x, m);
        }
        r2 = x;
    }
    N384 add(N384 a, const N384 &b) const {
        uint32_t carry = addTo(a, b);
        if (carry || cmp(a, m) >= 0) subFrom(a, m);
        return a;
    }
    N384 sub(N384 a, const N384 &b) const {
        if (subFrom(a, b)) addTo(a, m);
        return a;
    }
    N384 mul(const N384 &a, const N384 &b) const {  // a * b / R mod m (CIOS)
        uint32_t t[L + 2] = {0};
        for (int i = 0; i < L; ++i) {
            uint64_t c = 0;
            for (int j = 0; j < L; ++j) {
                c += (uint64_t)t[j] + (uint64_t)a.w[j] * b.w[i];
                t[j] = (uint32_t)c;
                c >>= 32;
            }
            c += t[L];
            t[L] = (uint32_t)c;
            t[L + 1] = (uint32_t)(c >> 32);
            uint32_t q = t[0] * n0;
            c = ((uint64_t)t[0] + (uint64_t)q * m.w[0]) >> 32;
            for (int j = 1; j < L; ++j) {
                c += (uint64_t)t[j] + (uint64_t)q * m.w[j];
                t[j - 1] = (uint32_t)c;
                c >>= 32;
            }
            c += t[L];
            t[L - 1] = (uint32_t)c;
            t[L] = t[L + 1] + (uint32_t)(c >> 32);
        }
        N384 r;
        std::memcpy(r.w, t, sizeof(r.w));
        if (t[L] || cmp(r, m) >= 0) subFrom(r, m);
        return r;
    }
    N384 to(const N384 &a) const { return mul(a, r2); }
    N384 from(const N384 &a) const {
        N384 one{};
        one.w[0] = 1;
        return mul(a, one);
    }
    N384 one() const {
        N384 o{};
        o.w[0] = 1;
        return to(o);
    }
    // a^(m-2): the inverse for a prime m (a in the Montgomery domain)
    N384 inverse(const N384 &a) const {
        N384 e = m, two{};
        two.w[0] = 2;
        subFrom(e, two);
        N384 r = one();
        for (int i = 383; i >= 0; --i) {
            r = mul(r, r);
            if (e.w[i / 32] >> (i % 32) & 1) r = mul(r, a);
        }
        return r;
    }
};

const Mod &fieldP() {
    static const Mod f(fromHex("fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffeffffffff0000000000000000ffffffff"));
    return f;
}

const Mod &orderN() {
    static const Mod f(fromHex("ffffffffffffffffffffffffffffffffffffffffffffffffc7634d81f4372ddf581a0db248b0a77aecec196accc52973"));
    return f;
}

const char *const P384_B = "b3312fa7e23ee7e4988e056be3f82d19181d9c6efe8141120314088f5013875ac656398d8a2ed19d2a85c8edd3ec2aef";
const char *const P384_GX = "aa87ca22be8b05378eb1c71ef320ad746e1d3b628ba79b9859f741e082542a385502f25dbf55296c3a545e3872760ab7";
const char *const P384_GY = "3617de4a96262c6f5d9e98bf9292dc29f8f41dbd289a147ce9da3113b5f0b8c00a60b1ce1d7e819d7a431d7c90ea0e5f";

// Jacobian coordinates, Montgomery domain; Z == 0: the point at infinity
struct Pt {
    N384 x, y, z;
};

bool infinite(const Pt &p) { return isZero(p.z); }

// dbl-2001-b (a = -3)
Pt dbl(const Pt &p) {
    const Mod &f = fieldP();
    if (infinite(p) || isZero(p.y)) return Pt{};
    N384 delta = f.mul(p.z, p.z), gamma = f.mul(p.y, p.y), beta = f.mul(p.x, gamma);
    N384 t = f.mul(f.sub(p.x, delta), f.add(p.x, delta));
    N384 alpha = f.add(f.add(t, t), t);
    N384 beta4 = f.add(f.add(beta, beta), f.add(beta, beta));
    Pt r;
    r.x = f.sub(f.mul(alpha, alpha), f.add(beta4, beta4));
    N384 yz = f.add(p.y, p.z);
    r.z = f.sub(f.sub(f.mul(yz, yz), gamma), delta);
    N384 g2 = f.mul(gamma, gamma), g8 = f.add(g2, g2);
    g8 = f.add(g8, g8);
    g8 = f.add(g8, g8);
    r.y = f.sub(f.mul(alpha, f.sub(beta4, r.x)), g8);
    return r;
}

// add-2007-bl
Pt add(const Pt &p, const Pt &q) {
    const Mod &f = fieldP();
    if (infinite(p)) return q;
    if (infinite(q)) return p;
    N384 z1z1 = f.mul(p.z, p.z), z2z2 = f.mul(q.z, q.z);
    N384 u1 = f.mul(p.x, z2z2), u2 = f.mul(q.x, z1z1);
    N384 s1 = f.mul(f.mul(p.y, q.z), z2z2), s2 = f.mul(f.mul(q.y, p.z), z1z1);
    N384 h = f.sub(u2, u1), rr = f.sub(s2, s1);
    if (isZero(h)) return isZero(rr) ? dbl(p) : Pt{};
    rr = f.add(rr, rr);
    N384 h2 = f.add(h, h), i = f.mul(h2, h2), j = f.mul(h, i), v = f.mul(u1, i);
    Pt r;
    r.x = f.sub(f.sub(f.mul(rr, rr), j), f.add(v, v));
    N384 s1j = f.mul(s1, j);
    r.y = f.sub(f.mul(rr, f.sub(v, r.x)), f.add(s1j, s1j));
    N384 zz = f.add(p.z, q.z);
    r.z = f.mul(f.sub(f.sub(f.mul(zz, zz), z1z1), z2z2), h);
    return r;
}

// a*G + b*Q (Shamir's trick); a, b plain integers
Pt twoMul(const N384 &a, const Pt &g, const N384 &b, const Pt &q) {
    Pt gq = add(g, q), r{};
    for (int i = 383; i >= 0; --i) {
        r = dbl(r);
        bool ba = a.w[i / 32] >> (i % 32) & 1, bb = b.w[i / 32] >> (i % 32) & 1;
        if (ba && bb) r = add(r, gq);
        else if (ba) r = add(r, g);
        else if (bb) r = add(r, q);
    }
    return r;
}

bool onCurve(const N384 &x, const N384 &y) {  // Montgomery domain, affine
    const Mod &f = fieldP();
    N384 lhs = f.mul(y, y);
    N384 x3 = f.mul(f.mul(x, x), x);
    N384 rhs = f.add(f.sub(x3, f.add(f.add(x, x), x)), f.to(fromHex(P384_B)));
    return cmp(lhs, rhs) == 0;
}

bool verifyP384(const std::vector<uint8_t> &pubRaw, const std::vector<uint8_t> &digest, const std::vector<uint8_t> &sig) {
    const Mod &f = fieldP(), &n = orderN();
    if (pubRaw.size() != 97 || pubRaw[0] != 0x04) return false;
    N384 qx = fromBE(pubRaw.data() + 1), qy = fromBE(pubRaw.data() + 49);
    if (cmp(qx, f.m) >= 0 || cmp(qy, f.m) >= 0) return false;
    Pt q{f.to(qx), f.to(qy), f.one()};
    if (!onCurve(q.x, q.y)) return false;

    uint8_t rb[48], sb[48], zb[48] = {0};
    if (!decodeSignature(sig, rb, sb, 48)) return false;
    N384 r = fromBE(rb), s = fromBE(sb);
    if (isZero(r) || cmp(r, n.m) >= 0 || isZero(s) || cmp(s, n.m) >= 0) return false;
    // the leftmost 384 bits of the digest
    size_t take = digest.size() < 48 ? digest.size() : 48;
    std::memcpy(zb + 48 - take, digest.data(), take);
    N384 z = fromBE(zb);
    if (cmp(z, n.m) >= 0) subFrom(z, n.m);

    N384 w = n.inverse(n.to(s));
    N384 u1 = n.from(n.mul(n.to(z), w)), u2 = n.from(n.mul(n.to(r), w));
    Pt g{f.to(fromHex(P384_GX)), f.to(fromHex(P384_GY)), f.one()};
    Pt R = twoMul(u1, g, u2, q);
    if (infinite(R)) return false;
    // affine x = X / Z^2
    N384 zi = f.inverse(R.z);
    N384 x = f.from(f.mul(R.x, f.mul(zi, zi)));
    if (cmp(x, n.m) >= 0) subFrom(x, n.m);
    return cmp(x, r) == 0;
}

} // namespace

bool ecdsa_verify_digest(const std::string &curveOid, const std::vector<uint8_t> &pubRaw,
                         const std::vector<uint8_t> &digest, const std::vector<uint8_t> &derSig) {
    if (digest.empty()) return false;
    if (curveOid == CurveP256) return verifyP256(pubRaw, digest, derSig);
    if (curveOid == CurveP384) return verifyP384(pubRaw, digest, derSig);
    return false;  // other curves: fail closed
}

} // namespace netplus
+25 −0
Original line number Diff line number Diff line
#pragma once

// ECDSA signature verification over an already computed digest, for the
// curves X.509 chains use: P-256 (prime256v1) and P-384 (secp384r1) --
// Let's Encrypt's ECDSA hierarchy (Root YE, YE1, ...) signs with P-384 and
// SHA-384. Verification only: everything here is public data, so the P-384
// arithmetic doesn't try to be constant-time.

#include <cstdint>
#include <string>
#include <vector>

namespace netplus {

constexpr const char *CurveP256 = "1.2.840.10045.3.1.7";
constexpr const char *CurveP384 = "1.3.132.0.34";

// curveOid: CurveP256 or CurveP384; pubRaw: the uncompressed point
// 0x04 || X || Y; digest: the message's hash (SHA-256/384, used as FIPS
// 186-4 says: its leftmost bits up to the order's length); derSig: the
// DER SEQUENCE { r INTEGER, s INTEGER }
bool ecdsa_verify_digest(const std::string &curveOid, const std::vector<uint8_t> &pubRaw,
                         const std::vector<uint8_t> &digest, const std::vector<uint8_t> &derSig);

} // namespace netplus
+1 −0
Original line number Diff line number Diff line
@@ -163,5 +163,6 @@ namespace netplus {
        friend class rsa_pss_sha256;
        friend class rsa_pss_sha384;
        friend class rsa_pkcs1_sha256;
        friend class rsa_pkcs1_sha384;
    };
};
+102 −0
Original line number Diff line number Diff line
#include "rsa_pkcs1_sha384.h"
#include "sha.h"
#include <cstring>

namespace netplus {

// DER encoding of DigestInfo for SHA-384 (RFC 8017 Section 9.2, Note 1)
// SEQUENCE { SEQUENCE { OID sha384, NULL }, OCTET STRING hash }
static const uint8_t SHA384_DIGEST_INFO_PREFIX[] = {
    0x30, 0x41,             // SEQUENCE (65 bytes)
    0x30, 0x0d,             //   SEQUENCE (13 bytes)
    0x06, 0x09,             //     OID (9 bytes) = 2.16.840.1.101.3.4.2.2 (SHA-384)
    0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x02,
    0x05, 0x00,             //     NULL
    0x04, 0x30              //   OCTET STRING (48 bytes = SHA-384 hash follows)
};

// EMSA-PKCS1-v1_5-ENCODE (RFC 8017 Section 9.2)
std::vector<uint8_t> rsa_pkcs1_sha384::emsa_pkcs1_v15_encode(
    const std::vector<uint8_t>& mHash, size_t emLen)
{
    // T = DigestInfo prefix || Hash
    const size_t tLen = sizeof(SHA384_DIGEST_INFO_PREFIX) + mHash.size();

    if (emLen < tLen + 11)
        return {};

    // EM = 0x00 || 0x01 || PS || 0x00 || T
    // PS = 0xFF bytes, length = emLen - tLen - 3
    std::vector<uint8_t> EM(emLen);
    size_t idx = 0;

    EM[idx++] = 0x00;
    EM[idx++] = 0x01;

    size_t psLen = emLen - tLen - 3;
    std::memset(&EM[idx], 0xFF, psLen);
    idx += psLen;

    EM[idx++] = 0x00;

    std::memcpy(&EM[idx], SHA384_DIGEST_INFO_PREFIX, sizeof(SHA384_DIGEST_INFO_PREFIX));
    idx += sizeof(SHA384_DIGEST_INFO_PREFIX);

    std::memcpy(&EM[idx], mHash.data(), mHash.size());

    return EM;
}

// RSASSA-PKCS1-v1_5-SIGN (RFC 8017 Section 8.2.1)
std::vector<uint8_t> rsa_pkcs1_sha384::sign(netplus::rsa& key,
                                            const std::vector<uint8_t>& message)
{
    // Step 1: Hash the message with SHA-384
    std::vector<uint8_t> mHash = sha384_hash(message);
    if (mHash.size() != 48)
        return {};

    // Step 2: Encode to EMSA-PKCS1-v1_5
    size_t modBits = key.n.bitLength();
    size_t k = (modBits + 7) / 8;  // RSA modulus length in bytes

    std::vector<uint8_t> EM = emsa_pkcs1_v15_encode(mHash, k);
    if (EM.empty())
        return {};

    // Step 3: RSA signature primitive: s = m^d mod n
    rsa::bigInt m = rsa::bytesToBigIntBE(EM);
    rsa::bigInt s = rsa::modPow(m, key.d, key.n);

    // Step 4: Convert to byte string
    std::vector<uint8_t> out = rsa::bigIntToBytesBE(s, k);
    return out;
}

// RSASSA-PKCS1-v1_5-VERIFY (RFC 8017 Section 8.2.2)
bool rsa_pkcs1_sha384::verify(netplus::rsa& key,
                              const std::vector<uint8_t>& message,
                              const std::vector<uint8_t>& signature)
{
    std::vector<uint8_t> mHash = sha384_hash(message);
    if (mHash.size() != 48)
        return false;

    size_t modBits = key.n.bitLength();
    size_t k = (modBits + 7) / 8;
    if (signature.empty() || signature.size() != k)
        return false;

    std::vector<uint8_t> EM = emsa_pkcs1_v15_encode(mHash, k);
    if (EM.empty())
        return false;

    // RSA verification primitive: m = s^e mod n
    rsa::bigInt s = rsa::bytesToBigIntBE(signature);
    rsa::bigInt m = rsa::modPow(s, key.e, key.n);

    std::vector<uint8_t> EM2 = rsa::bigIntToBytesBE(m, k);
    return EM2 == EM;
}

} // namespace netplus
Loading