Module: JWS

Defined in:
lib/jws.rb

Defined Under Namespace

Classes: DecodeError, VerificationError

Class Method Summary collapse

Class Method Details

.base64url_decode(str) ⇒ Object



23
24
25
26
# File 'lib/jws.rb', line 23

def base64url_decode(str)
  str += '=' *(4 - str.length.modulo(4))
  Base64.decode64(str.tr('-_', '+/'))
end

.base64url_encode(str) ⇒ Object



28
29
30
# File 'lib/jws.rb', line 28

def base64url_encode(str)
  Base64.encode64(str).tr('+/', '-_').gsub(/[\n=]/, '')
end

.decode(jws, key = nil, verify = true, options = {}, &keyfinder) ⇒ Object

Raises:



80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
# File 'lib/jws.rb', line 80

def decode(jws, key=nil, verify=true, options={}, &keyfinder)
  raise JWS::DecodeError.new('Nil JSON Web Signature') unless jws
  header, payload, signature, signing_input = decoded_segments(jws, verify)
  raise JWS::DecodeError.new('Not enough or too many segments') unless header && payload
  
  if verify
    algo, key = signature_algorithm_and_key(header, key, &keyfinder)
    if options[:algorithm] && algo != options[:algorithm]
      raise JWS::IncorrectAlgorithm.new('Expected a different algorithm')
    end
    verify_signature(algo, key, signing_input, signature)
  end

  return payload, header
end

.decode_header_and_payload(header_segment, payload_segment) ⇒ Object



66
67
68
69
70
# File 'lib/jws.rb', line 66

def decode_header_and_payload(header_segment, payload_segment)
  header = decode_json(base64url_decode(header_segment))
  payload = decode_json(base64url_decode(payload_segment))
  [header, payload]
end

.decode_json(encoded_json) ⇒ Object



126
127
128
129
130
# File 'lib/jws.rb', line 126

def decode_json(encoded_json)
  JSON.parse(encoded_json)
rescue JSON::ParseError
  raise JOSE::DecodeError.new("Invalid encoding")
end

.decoded_segments(jws, verify = true) ⇒ Object



72
73
74
75
76
77
78
# File 'lib/jws.rb', line 72

def decoded_segments(jws, verify=true)
  header_segment, payload_segment, crypto_segment = raw_segments(jws, verify)
  header, payload = decode_header_and_payload(header_segment, payload_segment)
  signature = base64url_decode(crypto_segment.to_s) if verify
  signing_input = [header_segment, payload_segment].join('.')
  [header, payload, signature, signing_input]
end

.encode(payload, key, algorithm = 'HS256', header_fields = {}) ⇒ Object



50
51
52
53
54
55
56
57
# File 'lib/jws.rb', line 50

def encode(payload, key, algorithm='HS256', header_fields={})
  algorithm ||= 'none'
  segments = []
  segments << encoded_header(algorithm, header_fields)
  segments << encoded_payload(payload)
  segments << encoded_signature(segments.join('.'), key, algorithm)
  segments.join('.')
end

.encode_json(raw) ⇒ Object



132
133
134
# File 'lib/jws.rb', line 132

def encode_json(raw)
  JSON.generate(raw)
end

.encoded_header(algorithm = 'HS256', header_fields = {}) ⇒ Object



32
33
34
35
# File 'lib/jws.rb', line 32

def encoded_header(algorithm='HS256', header_fields={})
  header = {'typ' => 'JWS', 'alg' => algorithm}.merge(header_fields)
  base64url_encode(encode_json(header))
end

.encoded_payload(payload) ⇒ Object



37
38
39
# File 'lib/jws.rb', line 37

def encoded_payload(payload)
  base64url_encode(encode_json(payload))
end

.encoded_signature(signing_input, key, algorithm) ⇒ Object



41
42
43
44
45
46
47
48
# File 'lib/jws.rb', line 41

def encoded_signature(signing_input, key, algorithm)
  if algorithm == 'none'
    ''
  else
    signature = sign(algorithm, signing_input, key)
    base64url_encode(signature)
  end
end

.raw_segments(jws, verify = true) ⇒ Object

Raises:



59
60
61
62
63
64
# File 'lib/jws.rb', line 59

def raw_segments(jws, verify=true)
  segments = jws.split('.')
  required_number_of_segments = verify ? [3] :[2,3]
  raise JWS::DecodeError.new('Not enough or too many segments') unless required_number_of_segments.include? segments.length
  segments
end

.secure_compare(a, b) ⇒ Object



117
118
119
120
121
122
123
124
# File 'lib/jws.rb', line 117

def secure_compare(a, b)
  return false if a.nil? || b.nil? || a.empty? || b.empty? || a.bytesize != b.bytesize
  l = a.unpack "C#{a.bytesize}"

  res = 0
  b.each_byte { |byte| res |= byte ^ l.shift }
  res == 0
end

.sign(algorithm, msg, key) ⇒ Object



11
12
13
14
15
16
17
# File 'lib/jws.rb', line 11

def sign(algorithm, msg, key)
  if ['HS256', 'HS384', 'HS512'].include?(algorithm)
    sign_hmac(algorithm, msg, key)
  else
    raise NotImplementedError.new("Unsupported signing mehtod")
  end
end

.sign_hmac(algorithm, msg, key) ⇒ Object



19
20
21
# File 'lib/jws.rb', line 19

def sign_hmac(algorithm, msg, key)
  OpenSSL::HMAC.digest(OpenSSL::Digest.new(algorithm.sub('HS', 'SHA')), key, msg)
end

.signature_algorithm_and_key(header, key, &keyfinder) ⇒ Object



96
97
98
99
100
101
# File 'lib/jws.rb', line 96

def signature_algorithm_and_key(header, key, &keyfinder)
  if keyfinder
    key = keyfinder.call(header)
  end
  [header['alg'], key]
end

.verify_signature(algo, key, signing_input, signature) ⇒ Object



103
104
105
106
107
108
109
110
111
112
113
114
115
# File 'lib/jws.rb', line 103

def verify_signature(algo, key, signing_input, signature)
  begin
    if ['HS256', 'HS384', 'HS512'].include?(algo)
      raise JWS::VerificationError.new('Signature verification failed') unless secure_compare(signature, sign_hmac(algo, signing_input, key))
    else
      raise JWS::VerificationError.new('Algorithm not supported')
    end
  rescue OpenSSL::Pkey::PkeyError
    raise JWS::VerificationError.new('Signature verification failed')
  ensure
    OpenSSL.errors.clear
  end
end