Module: RStyx::Keyring

Defined in:
lib/rstyx/keyring.rb

Defined Under Namespace

Classes: Authinfo, Certificate, FileWrapper, InfPrivateKey, InfPublicKey

Constant Summary collapse

MAX_MSG =
4096

Class Method Summary collapse

Class Method Details

.auth(fd, role, info, algs) ⇒ Object

Perform mutual authentication over a network connection fd. The role is the role of the connection, which may be either of the symbols :client or :server, info holds an Authinfo object containing this peer’s authentication information, and algs the bulk encryption algorithms supported by this peer. See Inferno’s keyring-auth(2) for more details on how this should work.



600
601
602
603
604
# File 'lib/rstyx/keyring.rb', line 600

def self.auth(fd, role, info, algs)
  res = basicauth(fd, info)
  setlinecrypt(fd, role, algs)
  return(res)
end

.basicauth(fd, info) ⇒ Object

Perform the Inferno authentication protocol, reading messages from and writing messages to fd, given the authentication information object info.



446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
# File 'lib/rstyx/keyring.rb', line 446

def self.basicauth(fd, info)
  secret = peerauth = nil
  begin
    # 1. Version negotiation
    sendmsg(fd, "1")
    buf = Keyring.getmsg(fd)
    vers = buf.to_i
    
    # 2. Check version
    if vers != 1 || buf.length > 4
      raise LocalAuthErr.new("incompatible authentication protocol")
    end

    if info.nil?
      raise LocalAuthErr.new("no authentication information")
    end

    if info.p.nil?
      raise LocalAuthErr.new("missing diffie hellman mod")
    end

    if info.alpha.nil?
      raise LocalAuthErr.new("missing diffie hellman base")
    end

    if info.mysk.nil? || info.mypk.nil? || info.cert.nil? || info.spk.nil?
      raise LocalAuthErr.new("invalid authentication information")
    end

    if info.p <= 0
      raise LocalAuthErr.new("negative modulus")
    end
    # 3. Diffie-Hellman authentication protocol.
    low = info.p >> (Keyring.bit_size(info.p) / 4)
    r0 = Keyring.randpq(low, info.p)
    alphar0 = mod_exp(info.alpha, r0, info.p)
    sendmsg(fd, Keyring.big2s(alphar0))
    sendmsg(fd, info.cert.to_s)
    sendmsg(fd, info.mypk.to_s)
    # 4. Receive peer's alpha**r1 mod p and the peer's certificate
    #    and public key.
    alphar1 = Keyring.s2big(Keyring.getmsg(fd))
    if info.p <= alphar1
      raise LocalAuthErr.new("implausible parameter value")
    end

    if alphar0 == alphar1
      raise LocalAuthErr.new("possible replay attack")
    end
    # 5. Verify the authenticity of the peer's certificate.
    hiscert = Certificate.from_s(getmsg(fd))
    hispkbuf = getmsg(fd)
    hispk = InfPublicKey.from_s(hispkbuf)
    unless verify(info.spk, hiscert, hispkbuf)
      raise LocalAuthErr.new("pk doesn't match certificate")
    end
    if hiscert.exp != 0 && (Time.at(hiscert.exp) <= Time.now)
      raise LocalAuthErr.new("certificate expired")
    end

    # 6. Send a certificate to the peer with alpha**r0 mod p and
    # alpha**r1 mod p.
    alphabuf = Keyring.big2s(alphar0) + Keyring.big2s(alphar1)
    alphacert = sign(info.mysk, 0, alphabuf)
    sendmsg(fd, alphacert.to_s)

    # 7. Receive the peer's certificate
    alphacert = Certificate.from_s(getmsg(fd))
    alphabuf = Keyring.big2s(alphar1) + Keyring.big2s(alphar0)
    # 8. Verify the certificate from the peer
    unless verify(hispk, alphacert, alphabuf)
      raise LocalAuthErr.new("signature did not match pk")
    end

    # alpha0r1 is the shared secret
    alpha0r1 = mod_exp(alphar1, r0, info.p)
    secret = ""
    val = alpha0r1
    while val > 0
      c = val % 256
      secret << c.chr
      val = val / 256
    end
    # Remove any leading nulls
    secret =~ /\0*(.*)/
    secret = $1
    peerauth = Authinfo.new(nil, hispk, hiscert, info.spk, info.alpha,
                            info.p)
    # 9. Send a protocol message containing OK back to the client.
    sendmsg(fd, "OK")
  rescue IOError => e
    raise LocalAuthErr.new("I/O error: #{e.message}")
  rescue InvalidCertificateException => e
    senderrmsg(fd, "remote: #{e.message}")
    raise e
  rescue InvalidKeyException => e
    senderrmsg(fd, "remote: #{e.message}")
    raise e
  rescue NoSuchAlgorithmException => e
    senderrmsg(fd, "remote: unsupported algorithm: #{e.message}")
    raise e
  rescue LocalAuthErr => e
    senderrmsg(fd, "remote: #{e.message}")
    raise e
  rescue RemoteAuthErr => e
    senderrmsg(fd, "missing your authentication data")
    raise AuthenticationException.new(e.message)
  end

  begin
    # 10. Receive an OK from the peer.
    until /OK/ =~ getmsg(fd)
    end
  rescue Exception => e
    raise AuthenticationException.new("i/o error: #{e.message}")
  end
  return([peerauth, secret])
end

.big2s(val) ⇒ Object

Convert a bignum val into a big-endian base64-encoded byte string



57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
# File 'lib/rstyx/keyring.rb', line 57

def self.big2s(val)
  str = ""
  while val > 0
    c = val % 256
    str = c.chr + str
    val = val / 256
  end
  # Force leading 0 byte for compatibility with older representation
  # See libinterp/keyring.c function bigtobase64.
  if str.length != 0 && ((str[0] & 0x80) != 0)
    str = "\0" + str
  end
  str = [str].pack("m")
  # Ruby will add newlines into the Base64 representation.  Remove
  # them; they're useless.
  str.tr!("\n", "")
  return(str)
end

.bit_size(i) ⇒ Object

Determine the size of i in bits.



95
96
97
98
99
100
101
102
103
# File 'lib/rstyx/keyring.rb', line 95

def self.bit_size(i)
  hibit = i.size * 8 - 1
  while (i[hibit] == 0)
    hibit -= 1
    break if hibit < 0
  end

  return(hibit + 1)
end

.getmsg(fd) ⇒ Object

Get a message from fd. A message is defined as a four-digit number (representing the size), followed by a newline, followed by a number of bytes equal to the number. The number may be preceded by an exclamation point, in which the data becomes the message sent as a RemoteAuthErr exception’s error text.



141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
# File 'lib/rstyx/keyring.rb', line 141

def self.getmsg(fd)
  num = fd.read(5)
  if num[4..4] != "\n"
    raise IOError.new("bad message syntax")
  end

  iserr = false
  i = nil
  n = 0
  if num[0..0] == '!'
    iserr = true
    n = num[1,3].to_i
  else
    n = num.to_i
  end

  if n < 0 || n > MAX_MSG
    raise IOError.new("message syntax")
  end
  z = fd.read(n)
  if iserr
    raise RemoteAuthErr.new(z)
  end
  return(z)
end

.mod_exp(b, e, m) ⇒ Object

Modular exponentiation. Computes b^e mod m using the square and multiply method.



80
81
82
83
84
85
86
87
88
89
90
# File 'lib/rstyx/keyring.rb', line 80

def self.mod_exp(b, e, m)
  res = 1
  while e > 0
    if e[0] == 1
      res = (res * b) % m
    end
    e >>= 1
    b = (b*b) % m
  end
  return(res)
end

.randpq(p, q) ⇒ Object

Generate a random number between p and q. Uses OpenSSL::Random to generate the random number.



109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
# File 'lib/rstyx/keyring.rb', line 109

def self.randpq(p, q)
  if p > q
    t = p
    p = q
    q = t
  end
  diff = q - p
  if diff < 2
    raise RuntimeError.new("range must be at least two")
  end
  l = Keyring.bit_size(diff)
  t = 1 << l
  l = (l + 7) & ~7          # nearest byte
  slop = t % diff
  r = -1
  while r < slop
    buf = OpenSSL::Random.random_bytes(l)
    r = 0
    buf.each_byte do |b|
      r = r*256 + b
    end
  end
  return((r % diff) + p)
end

.rsadecrypt(sk, data) ⇒ Object

Perform RSA decryption given a private key sk and ciphertext data.



619
620
621
622
623
624
625
626
627
# File 'lib/rstyx/keyring.rb', line 619

def self.rsadecrypt(sk, data)
  p = sk.p.to_i
  v1 = data % p
  q = sk.q.to_i
  v2 = data % q
  v1 = mod_exp(v1, sk.dmp1.to_i, p)
  v2 = mod_exp(v2, sk.dmq1.to_i, q)
  return((((v2 - v1)*sk.iqmp) % q)*p + v1)
end

.rsaencrypt(pk, data) ⇒ Object

Perform RSA encryption given a (public or private) key pk and plaintext data represented as a BigInteger. Returns the RSA ciphertext as a BigInteger



611
612
613
# File 'lib/rstyx/keyring.rb', line 611

def self.rsaencrypt(pk, data)
  return(mod_exp(data, pk.e.to_i, pk.n.to_i))
end

.rsaverify(m, sig, key) ⇒ Object

Verify an RSA signature, given the hash m, the signature data sig, and the signer public key key.



633
634
635
# File 'lib/rstyx/keyring.rb', line 633

def self.rsaverify(m, sig, key)
  return(rsaencrypt(key, sig) == m)
end

.s2big(str2) ⇒ Object

Convert a big-endian, base64-encoded byte string str2 into a bignum



49
50
51
52
# File 'lib/rstyx/keyring.rb', line 49

def self.s2big(str2)
  str = str2.unpack("m")[0]
  return(str2big(str))
end

.senderrmsg(fd, data) ⇒ Object

Send an error message to fd, prefixed by a ! and a three digit size.



179
180
181
182
# File 'lib/rstyx/keyring.rb', line 179

def self.senderrmsg(fd, data)
  fd.printf("!%03d\n", data.length)
  fd.write(data)
end

.sendmsg(fd, data) ⇒ Object

Send a message to fd, prefixed by a four-digit zero-padded size.



170
171
172
173
# File 'lib/rstyx/keyring.rb', line 170

def self.sendmsg(fd, data)
  fd.printf("%04d\n", data.length)
  fd.write(data)
end

.setlinecrypt(fd, role, algs) ⇒ Object

Set cryptographic algorithms in use, given a connection object fd a role role (which may be :client or :server), and a list of algorithms. Only the first algorithm listed is in use. This is a stub until we can figure out exactly how Inferno does cryptographic protocol negotiation.



572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
# File 'lib/rstyx/keyring.rb', line 572

def self.setlinecrypt(fd, role, algs)
  if role == :client
    if (!algs.nil? && algs.length > 0)
      alg = algs[0]
    else
      alg = "none"          # we need to either figure out how to use SSL without its handshake or write our own code to do cryptography manually.
    end
    sendmsg(fd, alg)
  elsif role == :server
    alg = self.getmsg(fd)
    if alg != "none"
      raise IOError.new("unsupported algorithm: " + alg)
    end
  else
    raise IOException.new("invalid role #{role.to_s}")
  end
  return(alg)
end

.sign(sk, exp, a) ⇒ Object

Sign a certificate, given a private key sk, an expiration time exp in seconds from the Epoch, and the data to sign a.



658
659
660
661
662
663
664
665
# File 'lib/rstyx/keyring.rb', line 658

def self.sign(sk, exp, a)
  sha1 = Digest::SHA1.new
  sha1.update(a)
  sha1.update("#{sk.owner} #{exp}")
  digest = str2big(sha1.digest)
  sig = rsadecrypt(sk.sk, digest)
  return(Certificate.new("rsa", "sha1", sk.owner, exp, sig))
end

.str2big(str) ⇒ Object

Convert a big-endian byte string str into a bignum.



38
39
40
41
42
43
44
# File 'lib/rstyx/keyring.rb', line 38

def self.str2big(str)
  val = 0
  str.each_byte do |b|
    val = val*256 + b
  end
  return(val)
end

.verify(pk, c, a) ⇒ Object

Verify a certificate c given the public key of the signer pk, and the actual data of the certificate a.



641
642
643
644
645
646
647
648
649
650
651
652
# File 'lib/rstyx/keyring.rb', line 641

def self.verify(pk, c, a)
  # Check if the certificate algorithm is supported.  At the moment
  # only RSA signatures over SHA-1 are supported.
  unless c.sa == "rsa" && (c.ha == "sha1" || c.ha == "sha")
    return(false)
  end
  sha1 = Digest::SHA1.new
  sha1.update(a)
  sha1.update("#{c.signer} #{c.exp}")
  val = str2big(sha1.digest)
  return(rsaverify(val, c.rsa, pk.pk))
end