Class: KnoxCall::DpopKeyPair
- Inherits:
-
Object
- Object
- KnoxCall::DpopKeyPair
- Defined in:
- lib/knoxcall/dpop.rb
Overview
DPoP proof generation (RFC 9449) — client side. Ruby port of knoxcall-node/src/auth/dpop.ts (see ../../PARITY.md §7).
Generates an ES256 (P-256) keypair and signs a fresh proof JWT per request (new jti/iat every call, htu stripped of query and fragment, ath bound to the access token when one is presented). Uses the openssl stdlib already required by the client — no new gem. The private key never leaves the process.
Instance Attribute Summary collapse
-
#public_jwk ⇒ Object
readonly
Returns the value of attribute public_jwk.
Class Method Summary collapse
-
.b64url(bytes) ⇒ Object
Unpadded base64url via pack, not the base64 gem — base64 left the stdlib default set in Ruby 3.4, and the README promises stdlib only.
-
.der_to_p1363(der) ⇒ Object
Convert an ASN.1/DER ECDSA signature to JOSE P1363 (r||s, 64 bytes).
- .generate ⇒ Object
Instance Method Summary collapse
-
#initialize(key) ⇒ DpopKeyPair
constructor
A new instance of DpopKeyPair.
-
#sign(method, url, access_token: nil, nonce: nil) ⇒ Object
Sign a DPoP proof JWT for one request (RFC 9449 §4.2).
-
#thumbprint ⇒ Object
RFC 7638 JWK thumbprint of the public key — the value the server binds tokens to as cnf.jkt.
Constructor Details
#initialize(key) ⇒ DpopKeyPair
Returns a new instance of DpopKeyPair.
21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
# File 'lib/knoxcall/dpop.rb', line 21 def initialize(key) @key = key point = key.public_key.to_bn.to_s(2) # uncompressed: 0x04 || X || Y unless point.bytesize == 65 && point.getbyte(0) == 4 raise Error, "unexpected EC public key encoding" end # Member order crv, kty, x, y matches the RFC 7638 canonical order, so # the same hash serves both the proof header and the thumbprint input. @public_jwk = { "crv" => "P-256", "kty" => "EC", "x" => self.class.b64url(point.byteslice(1, 32)), "y" => self.class.b64url(point.byteslice(33, 32)) }.freeze end |
Instance Attribute Details
#public_jwk ⇒ Object (readonly)
Returns the value of attribute public_jwk.
15 16 17 |
# File 'lib/knoxcall/dpop.rb', line 15 def public_jwk @public_jwk end |
Class Method Details
.b64url(bytes) ⇒ Object
Unpadded base64url via pack, not the base64 gem — base64 left the stdlib default set in Ruby 3.4, and the README promises stdlib only.
67 68 69 |
# File 'lib/knoxcall/dpop.rb', line 67 def self.b64url(bytes) [bytes].pack("m0").tr("+/", "-_").delete("=") end |
.der_to_p1363(der) ⇒ Object
Convert an ASN.1/DER ECDSA signature to JOSE P1363 (r||s, 64 bytes).
72 73 74 75 76 77 |
# File 'lib/knoxcall/dpop.rb', line 72 def self.der_to_p1363(der) seq = OpenSSL::ASN1.decode(der) r, s = seq.value.map { |int| int.value.to_s(2) } # BN → big-endian binary, no leading zeros raise Error, "invalid DER signature" if r.bytesize > 32 || s.bytesize > 32 r.rjust(32, "\0") + s.rjust(32, "\0") end |
.generate ⇒ Object
17 18 19 |
# File 'lib/knoxcall/dpop.rb', line 17 def self.generate new(OpenSSL::PKey::EC.generate("prime256v1")) end |
Instance Method Details
#sign(method, url, access_token: nil, nonce: nil) ⇒ Object
Sign a DPoP proof JWT for one request (RFC 9449 §4.2). The signature is ECDSA P-256 + SHA-256 in JOSE P1363 form (r||s, 64 bytes), matching the server verifier in src/lib/dpop-verifier.ts.
40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
# File 'lib/knoxcall/dpop.rb', line 40 def sign(method, url, access_token: nil, nonce: nil) htu = url.split("#", 2).first.split("?", 2).first header = { "alg" => "ES256", "typ" => "dpop+jwt", "jwk" => @public_jwk } payload = { "htm" => method.to_s.upcase, "htu" => htu, "iat" => Time.now.to_i, "jti" => self.class.b64url(SecureRandom.random_bytes(16)) } if access_token && !access_token.empty? payload["ath"] = self.class.b64url(OpenSSL::Digest::SHA256.digest(access_token)) end payload["nonce"] = nonce if nonce && !nonce.to_s.empty? signing_input = "#{self.class.b64url(JSON.generate(header))}.#{self.class.b64url(JSON.generate(payload))}" der = @key.sign(OpenSSL::Digest.new("SHA256"), signing_input) "#{signing_input}.#{self.class.b64url(self.class.der_to_p1363(der))}" end |
#thumbprint ⇒ Object
RFC 7638 JWK thumbprint of the public key — the value the server binds tokens to as cnf.jkt.
61 62 63 |
# File 'lib/knoxcall/dpop.rb', line 61 def thumbprint self.class.b64url(OpenSSL::Digest::SHA256.digest(JSON.generate(@public_jwk))) end |