|
1 | 1 | module quic |
2 | 2 |
|
| 3 | +import crypto.ecdsa |
| 4 | + |
3 | 5 | // CertificateEntry is one X.509 certificate plus its per-certificate |
4 | 6 | // extensions (RFC 8446 §4.4.2). v1 only speaks the X509 CertificateType — |
5 | 7 | // RawPublicKey (RFC 7250) is never negotiated (v1's EncryptedExtensions |
@@ -113,6 +115,66 @@ pub fn parse_certificate(body []u8) !ParsedCertificate { |
113 | 115 | } |
114 | 116 | } |
115 | 117 |
|
| 118 | +// encode_certificate constructs a complete Certificate handshake message |
| 119 | +// (RFC 8446 §4.4.2), framed via encode_handshake_message. `certificate_list` |
| 120 | +// is this server's own certificate chain, leaf-first (RFC 8446 §4.4.2's own |
| 121 | +// implicit ordering -- the peer's chain-validation walk, mirrored by this |
| 122 | +// codebase's own verify_certificate_chain, always treats the first entry as |
| 123 | +// the leaf). certificate_request_context is always encoded as empty: RFC |
| 124 | +// 8446 §4.4.2 states it is only non-empty "if this message is in response |
| 125 | +// to a CertificateRequest" -- "Otherwise (in the case of server |
| 126 | +// authentication), this field SHALL be zero length" -- and v1 is |
| 127 | +// server-authentication-only (client-cert auth is out of scope), so this |
| 128 | +// function never takes a caller-supplied context, the same scope |
| 129 | +// restriction parse_certificate's own doc comment already states for the |
| 130 | +// parse side. |
| 131 | +pub fn encode_certificate(certificate_list []CertificateEntry) ![]u8 { |
| 132 | + // RFC 8446 §4.4.2.4 (quoted in parse_certificate's own doc comment): |
| 133 | + // "the server MUST always provide a non-empty certificate_list" -- |
| 134 | + // enforced here on the encode side too, not just checked on the way |
| 135 | + // back in when a peer's Certificate is parsed. |
| 136 | + if certificate_list.len == 0 { |
| 137 | + return error('quic: Certificate certificate_list must not be empty (server certificate_list MUST always be non-empty, RFC 8446 §4.4.2.4)') |
| 138 | + } |
| 139 | + |
| 140 | + mut body := []u8{} |
| 141 | + body << u8(0) // certificate_request_context: always empty, see doc comment above |
| 142 | + |
| 143 | + mut list := []u8{} |
| 144 | + for entry in certificate_list { |
| 145 | + if entry.cert_data.len == 0 || entry.cert_data.len > 0xff_ffff { |
| 146 | + return error('quic: CertificateEntry cert_data length ${entry.cert_data.len} out of range (opaque cert_data<1..2^24-1>)') |
| 147 | + } |
| 148 | + list << u8(entry.cert_data.len >> 16) |
| 149 | + list << u8(entry.cert_data.len >> 8) |
| 150 | + list << u8(entry.cert_data.len) |
| 151 | + list << entry.cert_data |
| 152 | + // parse_certificate's own doc comment establishes that this |
| 153 | + // client's ClientHello offers neither status_request nor |
| 154 | + // signed_certificate_timestamp, so the only RFC 8446 §4.2-legal |
| 155 | + // CertificateEntry extensions for THIS codebase's peer are illegal |
| 156 | + // to send here (RFC 8446 §4.4.2: "Extensions in the Certificate |
| 157 | + // message from the server MUST correspond to ones from the |
| 158 | + // ClientHello message") -- enforced here too, not just on the |
| 159 | + // parse side, so a caller can never accidentally construct a |
| 160 | + // message a compliant peer would reject. |
| 161 | + if entry.extensions.len != 0 { |
| 162 | + return error('quic: CertificateEntry.extensions must be empty -- this server never negotiates status_request or signed_certificate_timestamp (RFC 8446 §4.4.2)') |
| 163 | + } |
| 164 | + list << u8(0) // extensions length: always 0, see above |
| 165 | + list << u8(0) |
| 166 | + } |
| 167 | + if list.len > 0xff_ffff { |
| 168 | + return error('quic: Certificate certificate_list too large: ${list.len} bytes') |
| 169 | + } |
| 170 | + body << u8(list.len >> 16) |
| 171 | + body << u8(list.len >> 8) |
| 172 | + body << u8(list.len) |
| 173 | + body << list |
| 174 | + |
| 175 | + return encode_handshake_message(.certificate, body)! |
| 176 | +} |
| 177 | + |
116 | 178 | pub struct ParsedCertificateVerify { |
117 | 179 | pub: |
118 | 180 | algorithm u16 |
@@ -201,3 +263,56 @@ pub fn certificate_verify_signed_content(role CertificateVerifyRole, transcript_ |
201 | 263 | out << transcript_hash |
202 | 264 | return out |
203 | 265 | } |
| 266 | + |
| 267 | +// encode_certificate_verify constructs a complete CertificateVerify |
| 268 | +// handshake message (RFC 8446 §4.4.3) by SIGNING |
| 269 | +// certificate_verify_signed_content(.server, transcript_hash) with |
| 270 | +// `signing_key`, then framing the result via encode_handshake_message. v1 |
| 271 | +// is server-authentication-only (client CertificateVerify is never sent), |
| 272 | +// so this function always signs the `.server` context -- see |
| 273 | +// certificate_verify_signed_content's own doc comment for why the `.client` |
| 274 | +// variant exists at all without a real caller. |
| 275 | +// |
| 276 | +// Only sig_scheme_ecdsa_secp256r1_sha256 is wired up so far: rejected with |
| 277 | +// a clear "not implemented yet" error for any other algorithm rather than |
| 278 | +// silently producing a signature under the wrong scheme -- RSA-PSS signing |
| 279 | +// needs a still-missing mbedtls_pk_sign_ext V wrapper (only the verify side, |
| 280 | +// verify_rsa_pss_signature in net.mbedtls, exists today), tracked as |
| 281 | +// follow-up work within 13a, not built here. |
| 282 | +// |
| 283 | +// `signing_key` MUST be a P-256 (prime256v1) key -- the only curve this |
| 284 | +// codebase's own key generation/loading ever produces (Phase 1's scope |
| 285 | +// decision, `crypto.ecdsa`'s CurveOptions defaults to prime256v1 and no v1 |
| 286 | +// caller ever overrides it). crypto.ecdsa exposes no curve accessor to |
| 287 | +// verify this defensively at the V level; behavior for a caller-supplied |
| 288 | +// non-P-256 key is undefined by construction, not validated here -- the |
| 289 | +// same trust boundary this function's own signing_key parameter implies |
| 290 | +// for any local, non-peer-supplied cryptographic material. |
| 291 | +pub fn encode_certificate_verify(algorithm u16, signing_key ecdsa.PrivateKey, transcript_hash []u8) ![]u8 { |
| 292 | + if algorithm != sig_scheme_ecdsa_secp256r1_sha256 { |
| 293 | + return error('quic: CertificateVerify signing for algorithm 0x${algorithm:04x} is not implemented yet (only ecdsa_secp256r1_sha256 is wired up)') |
| 294 | + } |
| 295 | + |
| 296 | + content := certificate_verify_signed_content(.server, transcript_hash) |
| 297 | + // PrivateKey.sign's default hash_config (.with_recommended_hash) picks |
| 298 | + // SHA-256 for a 256-bit (P-256) key -- see default_digest in |
| 299 | + // vlib/crypto/ecdsa/ecdsa.v, keyed off the key's own bit size, matching |
| 300 | + // exactly what sig_scheme_ecdsa_secp256r1_sha256 requires. The |
| 301 | + // resulting signature is OpenSSL's standard ASN.1 DER ECDSA-Sig-Value |
| 302 | + // encoding, the same format net.mbedtls's verify_ecdsa_signature (used |
| 303 | + // on the client-side verify path, tls13_certificate_chain.c.v) already |
| 304 | + // parses -- no reformatting needed between the two libraries. |
| 305 | + signature := signing_key.sign(content, hash_config: .with_recommended_hash)! |
| 306 | + |
| 307 | + mut body := []u8{} |
| 308 | + body << u8(algorithm >> 8) |
| 309 | + body << u8(algorithm) |
| 310 | + if signature.len > 0xffff { |
| 311 | + return error('quic: CertificateVerify signature too large: ${signature.len} bytes') |
| 312 | + } |
| 313 | + body << u8(signature.len >> 8) |
| 314 | + body << u8(signature.len) |
| 315 | + body << signature |
| 316 | + |
| 317 | + return encode_handshake_message(.certificate_verify, body)! |
| 318 | +} |
0 commit comments