Class: AtprotoAuth::DPoP::KeyManager

Inherits:
Object
  • Object
show all
Defined in:
lib/atproto_auth/dpop/key_manager.rb

Overview

Manages ES256 keypair generation and storage for DPoP proofs. Provides functionality to generate new keys and store them securely. Uses JOSE for cryptographic operations and key format handling.

Defined Under Namespace

Classes: KeyError

Constant Summary collapse

CURVE =

Default curve for ES256 key generation

"P-256"
ALGORITHM =

Default algorithm for key usage

"ES256"

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(keypair = nil) ⇒ KeyManager

Creates a new KeyManager instance with an optional existing keypair

Parameters:

  • keypair (JOSE::JWK, nil) (defaults to: nil)

    Optional existing keypair to use

Raises:

  • (KeyError)

    if the provided keypair is invalid



23
24
25
26
# File 'lib/atproto_auth/dpop/key_manager.rb', line 23

def initialize(keypair = nil)
  @keypair = keypair || generate_keypair
  validate_keypair!
end

Instance Attribute Details

#keypairJOSE::JWK (readonly)

Returns The current DPoP keypair.

Returns:

  • (JOSE::JWK)

    The current DPoP keypair



18
19
20
# File 'lib/atproto_auth/dpop/key_manager.rb', line 18

def keypair
  @keypair
end

Class Method Details

.from_jwk(jwk) ⇒ KeyManager

Creates a KeyManager instance from a JWK

Parameters:

  • jwk (Hash)

    JWK representation of a keypair

Returns:

Raises:



137
138
139
140
141
142
# File 'lib/atproto_auth/dpop/key_manager.rb', line 137

def self.from_jwk(jwk)
  keypair = JOSE::JWK.from_map(jwk)
  new(keypair)
rescue StandardError => e
  raise KeyError, "Failed to import key: #{e.message}"
end

Instance Method Details

#deep_stringify_keys(obj) ⇒ Object



98
99
100
101
102
103
104
105
106
107
108
109
# File 'lib/atproto_auth/dpop/key_manager.rb', line 98

def deep_stringify_keys(obj)
  case obj
  when Hash
    obj.each_with_object({}) do |(k, v), hash|
      hash[k.to_s] = deep_stringify_keys(v)
    end
  when Array
    obj.map { |v| deep_stringify_keys(v) }
  else
    obj
  end
end

#generate_keypairJOSE::JWK

Generates a new ES256 keypair for DPoP usage

Returns:

  • (JOSE::JWK)

    The newly generated keypair

Raises:

  • (KeyError)

    if key generation fails



31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# File 'lib/atproto_auth/dpop/key_manager.rb', line 31

def generate_keypair
  # Generate base keypair
  base_key = JOSE::JWK.generate_key([:ec, CURVE])
  base_map = base_key.to_map

  # Create new map with all required properties
  key_map = {
    "kty" => base_map["kty"],
    "crv" => base_map["crv"],
    "x" => base_map["x"],
    "y" => base_map["y"],
    "d" => base_map["d"],
    "use" => "sig",
    "kid" => generate_kid(base_map)
  }

  # Create new JWK with all properties
  JOSE::JWK.from_map(key_map)
rescue StandardError => e
  raise KeyError, "Failed to generate keypair: #{e.message}"
end

#public_jwkHash

Returns the public key in JWK format

Returns:

  • (Hash)

    JWK representation of the public key



55
56
57
58
59
60
61
62
63
# File 'lib/atproto_auth/dpop/key_manager.rb', line 55

def public_jwk
  jwk = @keypair.to_public.to_map.to_h
  # If somehow the properties aren't set, add them
  jwk["use"] ||= "sig"
  jwk["kid"] ||= generate_kid(jwk)
  jwk
rescue StandardError => e
  raise KeyError, "Failed to export public key: #{e.message}"
end

#sign(data) ⇒ String

Signs data using the private key

Parameters:

  • data (String)

    Data to sign

Returns:

  • (String)

    The signature

Raises:



69
70
71
72
73
# File 'lib/atproto_auth/dpop/key_manager.rb', line 69

def sign(data)
  @keypair.sign(data).compact
rescue StandardError => e
  raise KeyError, "Failed to sign data: #{e.message}"
end

#sign_segments(header, payload) ⇒ Object



75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
# File 'lib/atproto_auth/dpop/key_manager.rb', line 75

def sign_segments(header, payload)
  # Deep transform all keys to strings to avoid symbol comparison issues
  header = deep_stringify_keys(header)
  payload = deep_stringify_keys(payload)

  # Configure JOSE to use ES256 for signing
  signing_config = { "alg" => "ES256" }

  # Merge our header with JOSE's required fields
  full_header = header.merge(signing_config)

  # Convert payload to JSON string before signing
  payload_json = JSON.generate(payload)

  # Create the JWS with our header and payload
  jws = @keypair.sign(payload_json, full_header)

  # Get the compact serialization
  jws.compact
rescue StandardError => e
  raise KeyError, "Failed to sign segments: #{e.message}"
end

#to_jwk(include_private: false) ⇒ Hash

Exports the keypair in JWK format

Parameters:

  • include_private (Boolean) (defaults to: false)

    Whether to include private key

Returns:

  • (Hash)

    JWK representation of the keypair

Raises:



126
127
128
129
130
131
# File 'lib/atproto_auth/dpop/key_manager.rb', line 126

def to_jwk(include_private: false)
  key = include_private ? @keypair : @keypair.to_public
  key.to_map
rescue StandardError => e
  raise KeyError, "Failed to export key: #{e.message}"
end

#verify(signed_jws) ⇒ Boolean

Verifies a signed JWS

Parameters:

  • signed_jws (String)

    The complete signed JWS to verify

Returns:

  • (Boolean)

    True if signature is valid

Raises:



115
116
117
118
119
120
# File 'lib/atproto_auth/dpop/key_manager.rb', line 115

def verify(signed_jws)
  verified, _payload, = @keypair.verify(signed_jws)
  verified
rescue StandardError => e
  raise KeyError, "Failed to verify signature: #{e.message}"
end