├── .rspec ├── spec ├── spec_helper.rb ├── hex_spec.rb ├── yubikey │ ├── cert_chain_spec.rb │ ├── modhex_spec.rb │ ├── otp_spec.rb │ └── otp_verify_spec.rb └── yubikey_spec.rb ├── Gemfile ├── .travis.yml ├── .gitignore ├── Rakefile ├── lib ├── yubikey.rb ├── yubikey │ ├── hex.rb │ ├── modhex.rb │ ├── configuration.rb │ ├── otp.rb │ └── otp_verify.rb └── cert │ └── chain.pem ├── examples ├── otp_verify.rb ├── otp.rb └── config.rb ├── LICENSE ├── yubikey.gemspec └── README.md /.rspec: -------------------------------------------------------------------------------- 1 | --colour 2 | --format=nested 3 | --require spec_helper 4 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'rspec' 3 | 4 | $LOAD_PATH.unshift(File.dirname(__FILE__) + '/../lib') 5 | require 'yubikey' -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://www.rubygems.org" 2 | 3 | gemspec 4 | 5 | platforms :rbx do 6 | gem 'rubysl', '~> 2.0' # if using anything in the ruby standard library 7 | end -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | 3 | rvm: 4 | - 1.9.2 5 | - 1.9.3 6 | - 2.0.0 7 | - 2.1.0 8 | - jruby-19mode 9 | - rbx 10 | - ruby-head 11 | - jruby-head 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | Makefile 2 | *.o 3 | *.bundle 4 | *.so 5 | *.dll 6 | *.log 7 | pkg 8 | doc 9 | .DS_Store 10 | coverage 11 | tmp 12 | script 13 | .idea 14 | .rvmrc 15 | *.gem 16 | .project 17 | Gemfile.lock 18 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rake' 2 | require 'rspec/core/rake_task' 3 | require 'rdoc/task' 4 | 5 | $LOAD_PATH.unshift('lib') 6 | 7 | RDoc::Task.new do |rdoc| 8 | rdoc.rdoc_dir = 'doc' 9 | rdoc.title = 'yubikey' 10 | rdoc.main = 'README.rdoc' 11 | rdoc.rdoc_files.include('README*', 'lib/**/*.rb') 12 | end 13 | 14 | RSpec::Core::RakeTask.new do |t| 15 | t.rspec_opts = ['--options', 'spec/spec.opts'] 16 | end 17 | 18 | task :default => :spec -------------------------------------------------------------------------------- /lib/yubikey.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift(File.dirname(__FILE__)) unless 2 | $LOAD_PATH.include?(File.dirname(__FILE__)) || $LOAD_PATH.include?(File.expand_path(File.dirname(__FILE__))) 3 | 4 | require 'net/https' 5 | require 'openssl' 6 | 7 | require 'yubikey/configuration' 8 | require 'yubikey/hex' 9 | require 'yubikey/modhex' 10 | require 'yubikey/otp' 11 | require 'yubikey/otp_verify' 12 | 13 | module Yubikey 14 | extend Configuration 15 | end 16 | 17 | -------------------------------------------------------------------------------- /examples/otp_verify.rb: -------------------------------------------------------------------------------- 1 | require 'yubikey' 2 | 3 | begin 4 | api_id = 'YOUR_API_ID' 5 | api_key = 'YOUR_API_KEY' 6 | otp = 'THE_OTP_TO_TEST' 7 | 8 | token = Yubikey::OTP::Verify::new(:api_id => api_id, :api_key => api_key, :otp => otp) 9 | 10 | if token.valid? 11 | p 'valid OTP' 12 | elsif token.replayed? 13 | p 'replayed OTP' 14 | else 15 | p token.status 16 | end 17 | rescue Yubikey::OTP::InvalidOTPError 18 | p 'invalid OTP' 19 | end -------------------------------------------------------------------------------- /examples/otp.rb: -------------------------------------------------------------------------------- 1 | require 'yubikey' 2 | 3 | key = 'ecde18dbe76fbd0c33330f1c354871db' 4 | otp = 'dteffujehknhfjbrjnlnldnhcujvddbikngjrtgh' 5 | token = Yubikey::OTP.new(otp, key) 6 | 7 | p "Device public id: #{token.public_id}" 8 | p "Device secret id: #{token.secret_id}" 9 | p "Device insertions: #{token.insert_counter}" 10 | p "Session activation counter: #{token.session_counter}" 11 | p "Session timestamp: #{token.timestamp}" 12 | p "OTP random data: #{token.random_number}" -------------------------------------------------------------------------------- /examples/config.rb: -------------------------------------------------------------------------------- 1 | require 'yubikey' 2 | 3 | begin 4 | 5 | Yubikey.configure do |config| 6 | config.api_id = 'YOUR_API_ID' 7 | config.api_key = 'YOUR_API_KEY' 8 | end 9 | 10 | otp = 'THE_OTP_TO_TEST' 11 | 12 | token = Yubikey::OTP::Verify::new(:otp => otp) 13 | 14 | if token.valid? 15 | p 'valid OTP' 16 | elsif token.replayed? 17 | p 'replayed OTP' 18 | else 19 | p token.status 20 | end 21 | rescue Yubikey::OTP::InvalidOTPError 22 | p 'invalid OTP' 23 | end -------------------------------------------------------------------------------- /lib/yubikey/hex.rb: -------------------------------------------------------------------------------- 1 | class String 2 | 3 | # Convert hex string to binary 4 | def to_bin 5 | [self].pack('H*') 6 | end 7 | 8 | # Convert binary string to hex 9 | def to_hex 10 | unpack('H*')[0] 11 | end 12 | 13 | # Check if the string is hex encoded 14 | def hex? 15 | self =~ /^[0-9a-fA-F]+$/ ? true : false 16 | end 17 | 18 | # Check if the string is modhex encoded 19 | def modhex? 20 | self =~ /^[cbdefghijklnrtuv]+$/ ? true : false 21 | end 22 | 23 | end -------------------------------------------------------------------------------- /lib/yubikey/modhex.rb: -------------------------------------------------------------------------------- 1 | module Yubikey::ModHex 2 | 3 | TRANS = 'cbdefghijklnrtuv'.split(//) 4 | 5 | # Decode a ModHex string into binary data 6 | def self.decode(modhex_string) 7 | raise ArgumentError, "ModHex string length is not even" unless modhex_string.length % 2 == 0 8 | 9 | chars = 'cbdefghijklnrtuv' 10 | result = "" 11 | modhex_string.scan(/../).each do |c| 12 | result += (chars.index(c[0]) * 16 + chars.index(c[1])).chr 13 | end 14 | result 15 | end 16 | 17 | # Encode a binary string into ModHex 18 | def self.encode(string) 19 | result = '' 20 | 21 | string.each_byte do |b| 22 | result <<= TRANS[(b >> 4) & 0xF] 23 | result <<= TRANS[b & 0xF] 24 | end 25 | 26 | result 27 | end 28 | 29 | end # Yubikey::ModHex -------------------------------------------------------------------------------- /spec/hex_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: US-ASCII 2 | describe 'hex' do 3 | it 'encodes binary to hex' do 4 | "i\266H\034\213\253\242\266\016\217\"\027\233X\315V".to_hex. 5 | should == '69b6481c8baba2b60e8f22179b58cd56' 6 | 7 | "\354\336\030\333\347o\275\f33\017\0345Hq\333".to_hex. 8 | should == 'ecde18dbe76fbd0c33330f1c354871db' 9 | end 10 | 11 | it 'decodes hex to binary' do 12 | '69b6481c8baba2b60e8f22179b58cd56'.to_bin. 13 | should == "i\266H\034\213\253\242\266\016\217\"\027\233X\315V" 14 | 15 | 'ecde18dbe76fbd0c33330f1c354871db'.to_bin. 16 | should == "\354\336\030\333\347o\275\f33\017\0345Hq\333" 17 | end 18 | 19 | it 'detects if a string is hex' do 20 | 'ecde18dbe76fbd0c33330f1c354871db'.hex?.should be_true 21 | 'dteffujehknhfjbrjnlnldnhcujvddbikngjrtgh'.modhex?.should be_true 22 | 23 | 'foobar'.hex?.should be_false 24 | 'test'.modhex?.should be_false 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /spec/yubikey/cert_chain_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/../spec_helper.rb' 2 | 3 | describe "Cert Chain" do 4 | before do 5 | @cert_store = OpenSSL::X509::Store.new 6 | @cert_store.add_file Yubikey.certificate_chain 7 | end 8 | 9 | it "should accept cert from api.yubico.com" do 10 | uri = URI.parse('https://api.yubico.com/') 11 | 12 | http = Net::HTTP.new(uri.host,uri.port) 13 | http.use_ssl = true 14 | http.verify_mode = OpenSSL::SSL::VERIFY_PEER 15 | http.cert_store = @cert_store 16 | 17 | req = Net::HTTP::Get.new(uri.request_uri) 18 | 19 | expect {http.request(req)}.to_not raise_error() 20 | end 21 | 22 | it "should not accept cert from anywhere else" do 23 | uri = URI.parse('https://www.google.com/') 24 | 25 | http = Net::HTTP.new(uri.host,uri.port) 26 | http.use_ssl = true 27 | http.verify_mode = OpenSSL::SSL::VERIFY_PEER 28 | http.cert_store = @cert_store 29 | 30 | req = Net::HTTP::Get.new(uri.request_uri) 31 | 32 | expect {http.request(req)}.to raise_error(OpenSSL::SSL::SSLError) 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2009 Jonathan Rudenberg 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /spec/yubikey_spec.rb: -------------------------------------------------------------------------------- 1 | describe Yubikey do 2 | after do 3 | Yubikey.reset 4 | end 5 | 6 | describe ".api_id" do 7 | it "should return the default api_id" do 8 | Yubikey.api_id.should == Yubikey::Configuration::DEFAULT_API_ID 9 | end 10 | end 11 | 12 | describe ".api_id=" do 13 | it "should set the api_id" do 14 | Yubikey.api_id = 8094 15 | Yubikey.api_id.should == 8094 16 | end 17 | end 18 | 19 | describe ".api_key" do 20 | it "should return the default api_key" do 21 | Yubikey.api_key.should == Yubikey::Configuration::DEFAULT_API_KEY 22 | end 23 | end 24 | 25 | describe ".api_key=" do 26 | it "should set the api_key" do 27 | Yubikey.api_key = 'NISwCZBQ0gTbuXbRGWAf4km5xXg=' 28 | Yubikey.api_key.should == 'NISwCZBQ0gTbuXbRGWAf4km5xXg=' 29 | end 30 | end 31 | 32 | describe ".configure" do 33 | 34 | Yubikey::Configuration::VALID_OPTIONS_KEYS.each do |key| 35 | 36 | it "should set the #{key}" do 37 | Yubikey.configure do |config| 38 | config.send("#{key}=", key) 39 | Yubikey.send(key).should == key 40 | end 41 | end 42 | end 43 | end 44 | end -------------------------------------------------------------------------------- /spec/yubikey/modhex_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: US-ASCII 2 | describe 'Yubikey::Modhex' do 3 | 4 | it 'decodes modhex' do 5 | Yubikey::ModHex.decode('hknhfjbrjnlnldnhcujvddbikngjrtgh').should == "i\266H\034\213\253\242\266\016\217\"\027\233X\315V" 6 | Yubikey::ModHex.decode('urtubjtnuihvntcreeeecvbregfjibtn').should == "\354\336\030\333\347o\275\f33\017\0345Hq\333" 7 | 8 | Yubikey::ModHex.decode('dteffuje').should == "-4N\203" 9 | 10 | Yubikey::ModHex.decode('ifhgieif').should == 'test' 11 | Yubikey::ModHex.decode('hhhvhvhdhbid').should == 'foobar' 12 | 13 | Yubikey::ModHex.decode('cc').should == "\000" 14 | end 15 | 16 | it 'encode modhex' do 17 | Yubikey::ModHex.encode("i\266H\034\213\253\242\266\016\217\"\027\233X\315V").should == 'hknhfjbrjnlnldnhcujvddbikngjrtgh' 18 | Yubikey::ModHex.encode("\354\336\030\333\347o\275\f33\017\0345Hq\333").should == 'urtubjtnuihvntcreeeecvbregfjibtn' 19 | 20 | Yubikey::ModHex.encode("-4N\203").should == 'dteffuje' 21 | 22 | Yubikey::ModHex.encode('test').should == 'ifhgieif' 23 | Yubikey::ModHex.encode('foobar').should == 'hhhvhvhdhbid' 24 | 25 | Yubikey::ModHex.encode("\000").should == 'cc' 26 | end 27 | 28 | it 'raise an error when modhex string length uneven' do 29 | lambda { Yubikey::ModHex.decode('ifh') }.should raise_error(ArgumentError) 30 | end 31 | 32 | end 33 | -------------------------------------------------------------------------------- /yubikey.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |s| 2 | s.platform = Gem::Platform::RUBY 3 | s.name = "yubikey" 4 | s.version = "1.4.1" 5 | s.description = "A library to verify, decode, decrypt and parse Yubikey one-time passwords." 6 | s.summary = "Yubikey library for Ruby" 7 | 8 | s.authors = ["Jonathan Rudenberg"] 9 | s.email = "jon335@gmail.com" 10 | s.date = "2013-03-19" 11 | s.homepage = "https://github.com/titanous/yubikey" 12 | 13 | s.extra_rdoc_files = [ 14 | "LICENSE", 15 | "README.md" 16 | ] 17 | s.files = [ 18 | "examples/otp.rb", 19 | "lib/yubikey.rb", 20 | "lib/yubikey/configuration.rb", 21 | "lib/yubikey/hex.rb", 22 | "lib/yubikey/modhex.rb", 23 | "lib/yubikey/otp.rb", 24 | "lib/yubikey/otp_verify.rb", 25 | "lib/cert/chain.pem", 26 | "spec/hex_spec.rb", 27 | "spec/spec_helper.rb" 28 | ] 29 | s.rdoc_options = ["--title", "yubikey", "--main", "README.rdoc"] 30 | s.require_paths = ["lib"] 31 | s.rubyforge_project = "yubikey" 32 | 33 | # OpenSSL is now a dependancy 34 | s.add_dependency "jruby-openssl" if RUBY_PLATFORM == "java" 35 | s.add_dependency "openssl" if RUBY_PLATFORM == "ruby" 36 | 37 | s.add_development_dependency 'rake', ">= 0.8.7" 38 | s.add_development_dependency 'rdoc' 39 | s.add_development_dependency 'rspec', ">= 2.0" 40 | end 41 | -------------------------------------------------------------------------------- /spec/yubikey/otp_spec.rb: -------------------------------------------------------------------------------- 1 | describe 'Yubikey::OTP' do 2 | 3 | describe "should parse an OTP" do 4 | 5 | before :all do 6 | @token = Yubikey::OTP.new('enrlucvketdlfeknvrdggingjvrggeffenhevendbvgd', '4013a2c719c4e9734bbc63048b00e16b') 7 | end 8 | 9 | it "has the expected public_id" do 10 | @token.public_id.should == 'enrlucvketdl' 11 | end 12 | 13 | it "has the expected secret_id" do 14 | @token.secret_id.should == '912a644bbc7b' 15 | end 16 | 17 | it "has the expected insert_counter" do 18 | @token.insert_counter.should == 1 19 | end 20 | 21 | it "has the expected session_counter" do 22 | @token.session_counter.should == 0 23 | end 24 | 25 | it "has the expected timestamp" do 26 | @token.timestamp.should == 688051 27 | end 28 | 29 | it "has the expected random_number" do 30 | @token.random_number.should == 55936 31 | end 32 | 33 | end 34 | 35 | it 'should raise if key or otp invalid' do 36 | otp = 'hknhfjbrjnlnldnhcujvddbikngjrtgh' 37 | key = 'ecde18dbe76fbd0c33330f1c354871db' 38 | 39 | lambda { Yubikey::OTP.new(key, key) }.should raise_error(Yubikey::OTP::InvalidOTPError) 40 | lambda { Yubikey::OTP.new(otp, otp) }.should raise_error(Yubikey::OTP::InvalidKeyError) 41 | 42 | lambda { Yubikey::OTP.new(otp[0,31], key) }.should raise_error(Yubikey::OTP::InvalidOTPError) 43 | lambda { Yubikey::OTP.new(otp, key[0,31]) }.should raise_error(Yubikey::OTP::InvalidKeyError) 44 | 45 | lambda { Yubikey::OTP.new(otp[1,31]+'d', key) }.should raise_error(Yubikey::OTP::BadCRCError) 46 | end 47 | 48 | end 49 | -------------------------------------------------------------------------------- /lib/yubikey/configuration.rb: -------------------------------------------------------------------------------- 1 | module Yubikey 2 | # Defines constants and methods related to configuration 3 | module Configuration 4 | # An array of valid keys in the options hash when configuring a Yubikey::OTP::Verify 5 | VALID_OPTIONS_KEYS = [ 6 | :api_id, 7 | :url, 8 | :api_key, 9 | :certificate_chain, 10 | ].freeze 11 | 12 | # By default, we want to point to Yubicloud 13 | DEFAULT_API_URL = 'https://api.yubico.com/wsapi/2.0/' 14 | 15 | # By default, don't have an api_id 16 | DEFAULT_API_ID = nil 17 | 18 | # By default, don't have an api_key 19 | DEFAULT_API_KEY = nil 20 | 21 | # Default location of the Yubico certificate chain 22 | DEFAULT_CERTIFICATE_CHAIN = File.join(File.dirname(__FILE__), '../cert/chain.pem') 23 | 24 | # @private 25 | attr_accessor *VALID_OPTIONS_KEYS 26 | 27 | # When this module is extended, set all configuration options to their default values 28 | def self.extended(base) 29 | base.reset 30 | end 31 | 32 | # Convenience method to allow configuration options to be set in a block 33 | def configure 34 | yield self 35 | end 36 | 37 | # Create a hash of options and their values 38 | def options 39 | VALID_OPTIONS_KEYS.inject({}) do |option, key| 40 | option.merge!(key => send(key)) 41 | end 42 | end 43 | 44 | # Reset all configuration options to defaults 45 | def reset 46 | self.api_id = DEFAULT_API_ID 47 | self.url = DEFAULT_API_URL 48 | self.api_key = DEFAULT_API_KEY 49 | self.certificate_chain = DEFAULT_CERTIFICATE_CHAIN 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # yubikey 2 | 3 | [![Build Status](https://travis-ci.org/titanous/yubikey.png?branch=master)](https://travis-ci.org/titanous/yubikey) 4 | 5 | ## Description 6 | 7 | A library to verify, decode, decrypt and parse [Yubikey](http://www.yubico.com/home/index/) one-time passwords. 8 | 9 | ## Usage 10 | 11 | ### OTP Decryption 12 | 13 | ```ruby 14 | key = 'ecde18dbe76fbd0c33330f1c354871db' 15 | otp = 'dteffujehknhfjbrjnlnldnhcujvddbikngjrtgh' 16 | token = Yubikey::OTP.new(otp, key) 17 | 18 | p "Device public id: #{token.public_id}" #=> 'dteffuje' 19 | p "Device secret id: #{token.secret_id}" #=> '8792ebfe26cc' 20 | p "Device insertions: #{token.insert_counter}" #=> 19 21 | p "Session activation counter: #{token.session_counter}" #=> 17 22 | p "Session timestamp: #{token.timestamp}" #=> 49712 23 | p "OTP random data: #{token.random_number}" #=> 40904 24 | ``` 25 | 26 | ### OTP Verification 27 | Use your own `api_key` and `api_id`, which you can get at [yubico.com](https://upgrade.yubico.com/getapikey/). 28 | 29 | ```ruby 30 | begin 31 | otp = Yubikey::OTP::Verify.new(:api_id => 1234, 32 | :api_key => 'NiSwGZBQ0gTbwXbRGWAf4kM5xXg=', 33 | :otp => 'dteffujehknhfjbrjnlnldnhcujvddbikngjrtgh') 34 | 35 | if otp.valid? 36 | p 'valid OTP' 37 | elsif otp.replayed? 38 | p 'replayed OTP' 39 | end 40 | rescue Yubikey::OTP::InvalidOTPError 41 | p 'invalid OTP' 42 | end 43 | ``` 44 | 45 | ## Install 46 | 47 | Yubikey is available as a gem, to install it just install the gem: 48 | 49 | gem install yubikey 50 | 51 | If you're using Bundler, add the gem to Gemfile. 52 | 53 | gem 'yubikey' 54 | 55 | Then run bundle install. 56 | 57 | ## Copyright 58 | 59 | ### Ruby library 60 | 61 | Written by [Jonathan Rudenberg](https://github.com/titanous). Copyright (c) 2009 Jonathan Rudenberg 62 | 63 | The MIT License. See [LICENSE](https://github.com/titanous/yubikey/blob/master/LICENSE). 64 | 65 | ### Contributors 66 | 67 | [List of contributors](https://github.com/titanous/yubikey/graphs/contributors) -------------------------------------------------------------------------------- /lib/yubikey/otp.rb: -------------------------------------------------------------------------------- 1 | class Yubikey::OTP 2 | # first few modhex encoded characters of the OTP 3 | attr_reader :public_id 4 | # decrypted binary token 5 | attr_reader :token 6 | # binary AES key 7 | attr_reader :aes_key 8 | # hex id (encrypted in OTP) 9 | attr_reader :secret_id 10 | # integer that increments each time the Yubikey is plugged in 11 | attr_reader :insert_counter 12 | # ~8hz timer, reset on every insert 13 | attr_reader :timestamp 14 | # activation counter, reset on every insert 15 | attr_reader :session_counter 16 | # random integer used as padding and extra random noise 17 | attr_reader :random_number 18 | 19 | 20 | # Decode/decrypt a Yubikey one-time password 21 | # 22 | # [+otp+] ModHex encoded Yubikey OTP (at least 32 characters) 23 | # [+key+] 32-character hex AES key 24 | def initialize(otp, key) 25 | raise InvalidOTPError, 'OTP must be at least 32 characters of modhex' unless otp.modhex? && otp.length >= 32 26 | raise InvalidKeyError, 'Key must be 32 hex characters' unless key.hex? && key.length == 32 27 | 28 | # Get the public ID first 29 | @public_id = otp[0, 12] 30 | 31 | # Strip prefix so otp will decode (following from yubico-c library) 32 | otp = otp[-32,32] if otp.length > 32 33 | 34 | @token = Yubikey::ModHex.decode(otp[-32,32]) 35 | @aes_key = key.to_bin 36 | 37 | decrypter = OpenSSL::Cipher.new('AES-128-ECB').decrypt 38 | decrypter.key = @aes_key 39 | decrypter.padding = 0 40 | 41 | @token = decrypter.update(@token) + decrypter.final 42 | 43 | raise BadCRCError unless crc_valid? 44 | 45 | @secret_id, @insert_counter, @timestamp, @timestamp_lo, @session_counter, @random_number, @crc = @token.unpack('H12vvCCvv') 46 | @timestamp += @timestamp_lo * 65536 47 | end 48 | 49 | private 50 | 51 | def crc_valid? 52 | crc = 0xffff 53 | @token.each_byte do |b| 54 | crc ^= b & 0xff 55 | 8.times do 56 | test = (crc & 1) == 1 57 | crc >>= 1 58 | crc ^= 0x8408 if test 59 | end 60 | end 61 | crc == 0xf0b8 62 | end 63 | 64 | # :stopdoc: 65 | class InvalidOTPError < StandardError; end 66 | class InvalidKeyError < StandardError; end 67 | class BadCRCError < StandardError; end 68 | end # Yubikey::OTP 69 | -------------------------------------------------------------------------------- /lib/yubikey/otp_verify.rb: -------------------------------------------------------------------------------- 1 | require 'base64' 2 | require 'securerandom' 3 | require "net/http" 4 | require "uri" 5 | 6 | module Yubikey 7 | 8 | class OTP::Verify 9 | # The raw status from the Yubico server 10 | attr_reader :status 11 | 12 | def initialize(args) 13 | @api_key = args[:api_key] || Yubikey.api_key 14 | @api_id = args[:api_id] || Yubikey.api_id 15 | 16 | raise(ArgumentError, "Must supply API ID") if @api_id.nil? 17 | raise(ArgumentError, "Must supply API Key") if @api_key.nil? 18 | raise(ArgumentError, "Must supply OTP") if args[:otp].nil? 19 | 20 | @url = args[:url] || Yubikey.url 21 | @nonce = args[:nonce] || OTP::Verify.generate_nonce(32) 22 | 23 | @certificate_chain = args[:certificate_chain] || Yubikey.certificate_chain 24 | @cert_store = OpenSSL::X509::Store.new 25 | @cert_store.add_file @certificate_chain 26 | 27 | verify(args) 28 | end 29 | 30 | def valid? 31 | @status == 'OK' 32 | end 33 | 34 | def replayed? 35 | @status == 'REPLAYED_OTP' 36 | end 37 | 38 | private 39 | 40 | def verify(args) 41 | query = "id=#{@api_id}&otp=#{args[:otp]}&nonce=#{@nonce}" 42 | 43 | uri = URI.parse(@url) + 'verify' 44 | uri.query = query 45 | 46 | http = Net::HTTP.new(uri.host, uri.port) 47 | http.use_ssl = true 48 | http.verify_mode = OpenSSL::SSL::VERIFY_PEER 49 | http.cert_store = @cert_store 50 | 51 | req = Net::HTTP::Get.new(uri.request_uri) 52 | result = http.request(req).body 53 | 54 | @status = result[/status=(.*)$/,1].strip 55 | 56 | if @status == 'BAD_OTP' || @status == 'BACKEND_ERROR' 57 | raise OTP::InvalidOTPError, "Received error: #{@status}" 58 | end 59 | 60 | if ! verify_response(result) 61 | @status = 'BAD_RESPONSE' 62 | return 63 | end 64 | end 65 | 66 | def verify_response(result) 67 | signature = result[/^h=(.+)$/, 1].strip 68 | returned_nonce = result[/nonce=(.+)$/, 1] 69 | returned_nonce.strip! unless returned_nonce.nil? 70 | 71 | if @nonce != returned_nonce 72 | return false 73 | end 74 | 75 | generated_signature = OTP::Verify.generate_hmac(result, @api_key) 76 | 77 | return signature == generated_signature 78 | end 79 | 80 | 81 | def self.generate_nonce(length) 82 | return SecureRandom.hex length/2 83 | end 84 | 85 | 86 | def self.generate_hmac(response, api_key) 87 | response_params = response.split(' ') 88 | response_params.reject! do |p| 89 | p =~ /^h=(.+)$/ 90 | end 91 | 92 | response_string = response_params.sort.join('&') 93 | response_string.strip! 94 | 95 | hmac = OpenSSL::HMAC.digest('sha1', Base64.decode64(api_key), response_string) 96 | 97 | return Base64.encode64(hmac).strip 98 | end 99 | end # OTP::Verify 100 | end # Yubikey 101 | -------------------------------------------------------------------------------- /spec/yubikey/otp_verify_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/../spec_helper.rb' 2 | 3 | describe 'Yubikey::OTP::Verify' do 4 | 5 | before do 6 | @otp = 'ccccccbfueddkcnctfkrhdfrbtfutgtididiguttlctr' 7 | @id = 8094 8 | @key = 'NISwCZBQ0gTbuXbRGWAf4km5xXg=' 9 | @nonce = 'cEnCRWOnpFurshnksfcAEZvWoqdXlwbs' 10 | @response = "t=2012-08-01T16:09:08Z0899\n" + 11 | "otp=ccccccbfueddkcnctfkrhdfrbtfutgtididiguttlctr\n" + 12 | "nonce=cEnCRWOnpFurshnksfcAEZvWoqdXlwbs\n" 13 | 14 | @mock_http = double('http') 15 | @mock_http_get = double('http_get') 16 | Net::HTTP.stub(:new).with('api.yubico.com', 443).and_return(@mock_http) 17 | @mock_http.stub(:use_ssl=).with(true).and_return(nil) 18 | @mock_http.stub(:verify_mode=).with(OpenSSL::SSL::VERIFY_PEER).and_return(nil) 19 | @mock_http.stub(:cert_store=) 20 | @mock_http.stub(:request).with(@mock_http_get).and_return(@mock_http_get) 21 | 22 | Net::HTTP::Get.stub(:new).with(/id=#{@id}&otp=#{@otp}&nonce=[a-zA-Z0-9]{32}/).and_return(@mock_http_get) 23 | end 24 | 25 | it 'should verify a valid OTP' do 26 | ok_response = "#{@response}status=OK" 27 | hmac = Yubikey::OTP::Verify::generate_hmac(ok_response, @key) 28 | @mock_http_get.should_receive(:body).and_return("h=#{hmac}\n#{ok_response}") 29 | otp = Yubikey::OTP::Verify.new(:api_id => @id, :api_key => @key, :otp => @otp, :nonce => @nonce) 30 | otp.valid?.should == true 31 | otp.replayed?.should == false 32 | end 33 | 34 | it 'should verify a replayed OTP' do 35 | replayed_response = "#{@response}status=REPLAYED_OTP" 36 | hmac = Yubikey::OTP::Verify::generate_hmac(replayed_response, @key) 37 | @mock_http_get.should_receive(:body).and_return("h=#{hmac}\n#{replayed_response}") 38 | otp = Yubikey::OTP::Verify.new(:api_id => @id, :api_key => @key, :otp => @otp, :nonce => @nonce) 39 | otp.valid?.should == false 40 | otp.replayed?.should == true 41 | end 42 | 43 | it 'should raise on invalid OTP' do 44 | bad_response = "#{@response}status=BAD_OTP" 45 | hmac = Yubikey::OTP::Verify::generate_hmac(bad_response, @key) 46 | @mock_http_get.should_receive(:body).and_return("h=#{hmac}\n#{bad_response}") 47 | expect { otp = Yubikey::OTP::Verify.new(:api_id => @id, :api_key => @key, :otp => @otp, :nonce => @nonce) }.to raise_error(Yubikey::OTP::InvalidOTPError) 48 | end 49 | 50 | it 'should generate a correct hmac' do 51 | Yubikey::OTP::Verify::generate_hmac(@response, @key).should == 'sZqbbsXL5WIdqLNmr19/eq6acSM=' 52 | end 53 | 54 | it 'should raise on invalid parameters' do 55 | expect{ Yubikey::OTP::Verify.new({}) }.to raise_error(ArgumentError, "Must supply API ID") 56 | expect{ Yubikey::OTP::Verify.new({:api_id => 'foo'}) }.to raise_error(ArgumentError, "Must supply API Key") 57 | end 58 | 59 | context "with module configuration" do 60 | before do 61 | Yubikey.configure do |config| 62 | config.api_id = @id 63 | config.api_key = @key 64 | end 65 | end 66 | 67 | after do 68 | Yubikey.reset 69 | end 70 | 71 | it "should verify a valid OTP" do 72 | ok_response = "#{@response}status=OK" 73 | hmac = Yubikey::OTP::Verify::generate_hmac(ok_response, @key) 74 | @mock_http_get.should_receive(:body).and_return("h=#{hmac}\n#{ok_response}") 75 | otp = Yubikey::OTP::Verify.new(:otp => @otp, :nonce => @nonce) 76 | otp.valid?.should == true 77 | otp.replayed?.should == false 78 | end 79 | end 80 | 81 | end 82 | -------------------------------------------------------------------------------- /lib/cert/chain.pem: -------------------------------------------------------------------------------- 1 | --- 2 | Certificate chain 3 | 0 s:/O=api.yubico.com/OU=Domain Control Validated/CN=api.yubico.com 4 | i:/C=US/ST=Arizona/L=Scottsdale/O=GoDaddy.com, Inc./OU=http://certificates.godaddy.com/repository/CN=Go Daddy Secure Certification Authority/serialNumber=07969287 5 | -----BEGIN CERTIFICATE----- 6 | MIIFYzCCBEugAwIBAgIHBBh1dC6aUTANBgkqhkiG9w0BAQUFADCByjELMAkGA1UE 7 | BhMCVVMxEDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxGjAY 8 | BgNVBAoTEUdvRGFkZHkuY29tLCBJbmMuMTMwMQYDVQQLEypodHRwOi8vY2VydGlm 9 | aWNhdGVzLmdvZGFkZHkuY29tL3JlcG9zaXRvcnkxMDAuBgNVBAMTJ0dvIERhZGR5 10 | IFNlY3VyZSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTERMA8GA1UEBRMIMDc5Njky 11 | ODcwHhcNMTEwOTEyMjExMDA3WhcNMTMwOTEzMTM1NTA4WjBVMRcwFQYDVQQKEw5h 12 | cGkueXViaWNvLmNvbTEhMB8GA1UECxMYRG9tYWluIENvbnRyb2wgVmFsaWRhdGVk 13 | MRcwFQYDVQQDEw5hcGkueXViaWNvLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEP 14 | ADCCAQoCggEBAPBVcZF3r1U9rgeiUEBHTTuZY+3wzUj25ocwkc8lVN/wiNGYKaKf 15 | +Cbtc41w29FHc19i5ETtYWBDRwb3WWTHXTMf1ba/ntZt/mtI7Z1UiHUx1I4VRzHC 16 | 9HY3hrNw0IBfK07PRjBxx7uvGk0drH78SjNhlevk7mZK+trs+etOcMwET/WGAtHV 17 | bUgRqME9pDSrQgbsHOOS8FLHelC42lNaBzLBop+FuOE1WXFXj7zkMo4U1S568tSw 18 | dUYW8o4tZJ5I1FaZg549Qu89yBebe8dKdsp6PN3w2XAa9aylMNtUc6adxkJV6hkr 19 | CFIrpra3pcOrnVR1YMafE3Zy1vP+CoNiGBcCAwEAAaOCAcAwggG8MA8GA1UdEwEB 20 | /wQFMAMBAQAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMA4GA1UdDwEB 21 | /wQEAwIFoDAzBgNVHR8ELDAqMCigJqAkhiJodHRwOi8vY3JsLmdvZGFkZHkuY29t 22 | L2dkczEtNTYuY3JsMFMGA1UdIARMMEowSAYLYIZIAYb9bQEHFwEwOTA3BggrBgEF 23 | BQcCARYraHR0cDovL2NlcnRpZmljYXRlcy5nb2RhZGR5LmNvbS9yZXBvc2l0b3J5 24 | LzCBgAYIKwYBBQUHAQEEdDByMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5nb2Rh 25 | ZGR5LmNvbS8wSgYIKwYBBQUHMAKGPmh0dHA6Ly9jZXJ0aWZpY2F0ZXMuZ29kYWRk 26 | eS5jb20vcmVwb3NpdG9yeS9nZF9pbnRlcm1lZGlhdGUuY3J0MB8GA1UdIwQYMBaA 27 | FP2sYTKTbEXW4u6FX5q653aZaMznMC0GA1UdEQQmMCSCDmFwaS55dWJpY28uY29t 28 | ghJ3d3cuYXBpLnl1Ymljby5jb20wHQYDVR0OBBYEFB5gBqdncCyUhBcTJtYVnsG9 29 | tK8JMA0GCSqGSIb3DQEBBQUAA4IBAQBsZQLhz9/zlYBBg/M/htMW5dJLHxRwhfvG 30 | S4Dtd6a8o72n2TpEAfx7UnlLxoIT2kfohVIXwS7CfbT8yN42SvaxsCvqU+4q5uC8 31 | brNTDp21ihgPjMcGGKpLtJbito9t4Yy/5wrdSgdEtrQfaGROF5bTvrY5jlMq9azG 32 | 2qHAvdmB77Apk1PUe5myAPkpCYsLrq+j1cjDyTORfG2l43bE/kUeaIPYwH/jD7Vi 33 | 9Uj8ooWyaDBIDCR1yWhaWaLzZkjVhgt8eNUp7hrdOOvqZExHuGVaezkcwizkH1Eb 34 | nmcxLHaxO4/Di/QUmLjFoYMXWIag28O+XYIncY0HEFyfJDfXDbKA 35 | -----END CERTIFICATE----- 36 | 1 s:/C=US/ST=Arizona/L=Scottsdale/O=GoDaddy.com, Inc./OU=http://certificates.godaddy.com/repository/CN=Go Daddy Secure Certification Authority/serialNumber=07969287 37 | i:/C=US/O=The Go Daddy Group, Inc./OU=Go Daddy Class 2 Certification Authority 38 | -----BEGIN CERTIFICATE----- 39 | MIIE3jCCA8agAwIBAgICAwEwDQYJKoZIhvcNAQEFBQAwYzELMAkGA1UEBhMCVVMx 40 | ITAfBgNVBAoTGFRoZSBHbyBEYWRkeSBHcm91cCwgSW5jLjExMC8GA1UECxMoR28g 41 | RGFkZHkgQ2xhc3MgMiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wNjExMTYw 42 | MTU0MzdaFw0yNjExMTYwMTU0MzdaMIHKMQswCQYDVQQGEwJVUzEQMA4GA1UECBMH 43 | QXJpem9uYTETMBEGA1UEBxMKU2NvdHRzZGFsZTEaMBgGA1UEChMRR29EYWRkeS5j 44 | b20sIEluYy4xMzAxBgNVBAsTKmh0dHA6Ly9jZXJ0aWZpY2F0ZXMuZ29kYWRkeS5j 45 | b20vcmVwb3NpdG9yeTEwMC4GA1UEAxMnR28gRGFkZHkgU2VjdXJlIENlcnRpZmlj 46 | YXRpb24gQXV0aG9yaXR5MREwDwYDVQQFEwgwNzk2OTI4NzCCASIwDQYJKoZIhvcN 47 | AQEBBQADggEPADCCAQoCggEBAMQt1RWMnCZM7DI161+4WQFapmGBWTtwY6vj3D3H 48 | KrjJM9N55DrtPDAjhI6zMBS2sofDPZVUBJ7fmd0LJR4h3mUpfjWoqVTr9vcyOdQm 49 | VZWt7/v+WIbXnvQAjYwqDL1CBM6nPwT27oDyqu9SoWlm2r4arV3aLGbqGmu75RpR 50 | SgAvSMeYddi5Kcju+GZtCpyz8/x4fKL4o/K1w/O5epHBp+YlLpyo7RJlbmr2EkRT 51 | cDCVw5wrWCs9CHRK8r5RsL+H0EwnWGu1NcWdrxcx+AuP7q2BNgWJCJjPOq8lh8BJ 52 | 6qf9Z/dFjpfMFDniNoW1fho3/Rb2cRGadDAW/hOUoz+EDU8CAwEAAaOCATIwggEu 53 | MB0GA1UdDgQWBBT9rGEyk2xF1uLuhV+auud2mWjM5zAfBgNVHSMEGDAWgBTSxLDS 54 | kdRMEXGzYcs9of7dqGrU4zASBgNVHRMBAf8ECDAGAQH/AgEAMDMGCCsGAQUFBwEB 55 | BCcwJTAjBggrBgEFBQcwAYYXaHR0cDovL29jc3AuZ29kYWRkeS5jb20wRgYDVR0f 56 | BD8wPTA7oDmgN4Y1aHR0cDovL2NlcnRpZmljYXRlcy5nb2RhZGR5LmNvbS9yZXBv 57 | c2l0b3J5L2dkcm9vdC5jcmwwSwYDVR0gBEQwQjBABgRVHSAAMDgwNgYIKwYBBQUH 58 | AgEWKmh0dHA6Ly9jZXJ0aWZpY2F0ZXMuZ29kYWRkeS5jb20vcmVwb3NpdG9yeTAO 59 | BgNVHQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQEFBQADggEBANKGwOy9+aG2Z+5mC6IG 60 | OgRQjhVyrEp0lVPLN8tESe8HkGsz2ZbwlFalEzAFPIUyIXvJxwqoJKSQ3kbTJSMU 61 | A2fCENZvD117esyfxVgqwcSeIaha86ykRvOe5GPLL5CkKSkB2XIsKd83ASe8T+5o 62 | 0yGPwLPk9Qnt0hCqU7S+8MxZC9Y7lhyVJEnfzuz9p0iRFEUOOjZv2kWzRaJBydTX 63 | RE4+uXR21aITVSzGh6O1mawGhId/dQb8vxRMDsxuxN89txJx9OjxUUAiKEngHUuH 64 | qDTMBqLdElrRhjZkAzVvb3du6/KFUJheqwNTrZEjYx8WnM25sgVjOuH0aBsXBTWV 65 | U+4= 66 | -----END CERTIFICATE----- 67 | 2 s:/C=US/O=The Go Daddy Group, Inc./OU=Go Daddy Class 2 Certification Authority 68 | i:/L=ValiCert Validation Network/O=ValiCert, Inc./OU=ValiCert Class 2 Policy Validation Authority/CN=http://www.valicert.com//emailAddress=info@valicert.com 69 | -----BEGIN CERTIFICATE----- 70 | MIIE+zCCBGSgAwIBAgICAQ0wDQYJKoZIhvcNAQEFBQAwgbsxJDAiBgNVBAcTG1Zh 71 | bGlDZXJ0IFZhbGlkYXRpb24gTmV0d29yazEXMBUGA1UEChMOVmFsaUNlcnQsIElu 72 | Yy4xNTAzBgNVBAsTLFZhbGlDZXJ0IENsYXNzIDIgUG9saWN5IFZhbGlkYXRpb24g 73 | QXV0aG9yaXR5MSEwHwYDVQQDExhodHRwOi8vd3d3LnZhbGljZXJ0LmNvbS8xIDAe 74 | BgkqhkiG9w0BCQEWEWluZm9AdmFsaWNlcnQuY29tMB4XDTA0MDYyOTE3MDYyMFoX 75 | DTI0MDYyOTE3MDYyMFowYzELMAkGA1UEBhMCVVMxITAfBgNVBAoTGFRoZSBHbyBE 76 | YWRkeSBHcm91cCwgSW5jLjExMC8GA1UECxMoR28gRGFkZHkgQ2xhc3MgMiBDZXJ0 77 | aWZpY2F0aW9uIEF1dGhvcml0eTCCASAwDQYJKoZIhvcNAQEBBQADggENADCCAQgC 78 | ggEBAN6d1+pXGEmhW+vXX0iG6r7d/+TvZxz0ZWizV3GgXne77ZtJ6XCAPVYYYwhv 79 | 2vLM0D9/AlQiVBDYsoHUwHU9S3/Hd8M+eKsaA7Ugay9qK7HFiH7Eux6wwdhFJ2+q 80 | N1j3hybX2C32qRe3H3I2TqYXP2WYktsqbl2i/ojgC95/5Y0V4evLOtXiEqITLdiO 81 | r18SPaAIBQi2XKVlOARFmR6jYGB0xUGlcmIbYsUfb18aQr4CUWWoriMYavx4A6lN 82 | f4DD+qta/KFApMoZFv6yyO9ecw3ud72a9nmYvLEHZ6IVDd2gWMZEewo+YihfukEH 83 | U1jPEX44dMX4/7VpkI+EdOqXG68CAQOjggHhMIIB3TAdBgNVHQ4EFgQU0sSw0pHU 84 | TBFxs2HLPaH+3ahq1OMwgdIGA1UdIwSByjCBx6GBwaSBvjCBuzEkMCIGA1UEBxMb 85 | VmFsaUNlcnQgVmFsaWRhdGlvbiBOZXR3b3JrMRcwFQYDVQQKEw5WYWxpQ2VydCwg 86 | SW5jLjE1MDMGA1UECxMsVmFsaUNlcnQgQ2xhc3MgMiBQb2xpY3kgVmFsaWRhdGlv 87 | biBBdXRob3JpdHkxITAfBgNVBAMTGGh0dHA6Ly93d3cudmFsaWNlcnQuY29tLzEg 88 | MB4GCSqGSIb3DQEJARYRaW5mb0B2YWxpY2VydC5jb22CAQEwDwYDVR0TAQH/BAUw 89 | AwEB/zAzBggrBgEFBQcBAQQnMCUwIwYIKwYBBQUHMAGGF2h0dHA6Ly9vY3NwLmdv 90 | ZGFkZHkuY29tMEQGA1UdHwQ9MDswOaA3oDWGM2h0dHA6Ly9jZXJ0aWZpY2F0ZXMu 91 | Z29kYWRkeS5jb20vcmVwb3NpdG9yeS9yb290LmNybDBLBgNVHSAERDBCMEAGBFUd 92 | IAAwODA2BggrBgEFBQcCARYqaHR0cDovL2NlcnRpZmljYXRlcy5nb2RhZGR5LmNv 93 | bS9yZXBvc2l0b3J5MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQUFAAOBgQC1 94 | QPmnHfbq/qQaQlpE9xXUhUaJwL6e4+PrxeNYiY+Sn1eocSxI0YGyeR+sBjUZsE4O 95 | WBsUs5iB0QQeyAfJg594RAoYC5jcdnplDQ1tgMQLARzLrUc+cb53S8wGd9D0Vmsf 96 | SxOaFIqII6hR8INMqzW/Rn453HWkrugp++85j09VZw== 97 | -----END CERTIFICATE----- 98 | 3 s:/L=ValiCert Validation Network/O=ValiCert, Inc./OU=ValiCert Class 2 Policy Validation Authority/CN=http://www.valicert.com//emailAddress=info@valicert.com 99 | i:/L=ValiCert Validation Network/O=ValiCert, Inc./OU=ValiCert Class 2 Policy Validation Authority/CN=http://www.valicert.com//emailAddress=info@valicert.com 100 | -----BEGIN CERTIFICATE----- 101 | MIIC5zCCAlACAQEwDQYJKoZIhvcNAQEFBQAwgbsxJDAiBgNVBAcTG1ZhbGlDZXJ0 102 | IFZhbGlkYXRpb24gTmV0d29yazEXMBUGA1UEChMOVmFsaUNlcnQsIEluYy4xNTAz 103 | BgNVBAsTLFZhbGlDZXJ0IENsYXNzIDIgUG9saWN5IFZhbGlkYXRpb24gQXV0aG9y 104 | aXR5MSEwHwYDVQQDExhodHRwOi8vd3d3LnZhbGljZXJ0LmNvbS8xIDAeBgkqhkiG 105 | 9w0BCQEWEWluZm9AdmFsaWNlcnQuY29tMB4XDTk5MDYyNjAwMTk1NFoXDTE5MDYy 106 | NjAwMTk1NFowgbsxJDAiBgNVBAcTG1ZhbGlDZXJ0IFZhbGlkYXRpb24gTmV0d29y 107 | azEXMBUGA1UEChMOVmFsaUNlcnQsIEluYy4xNTAzBgNVBAsTLFZhbGlDZXJ0IENs 108 | YXNzIDIgUG9saWN5IFZhbGlkYXRpb24gQXV0aG9yaXR5MSEwHwYDVQQDExhodHRw 109 | Oi8vd3d3LnZhbGljZXJ0LmNvbS8xIDAeBgkqhkiG9w0BCQEWEWluZm9AdmFsaWNl 110 | cnQuY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDOOnHK5avIWZJV16vY 111 | dA757tn2VUdZZUcOBVXc65g2PFxTXdMwzzjsvUGJ7SVCCSRrCl6zfN1SLUzm1NZ9 112 | WlmpZdRJEy0kTRxQb7XBhVQ7/nHk01xC+YDgkRoKWzk2Z/M/VXwbP7RfZHM047QS 113 | v4dk+NoS/zcnwbNDu+97bi5p9wIDAQABMA0GCSqGSIb3DQEBBQUAA4GBADt/UG9v 114 | UJSZSWI4OB9L+KXIPqeCgfYrx+jFzug6EILLGACOTb2oWH+heQC1u+mNr0HZDzTu 115 | IYEZoDJJKPTEjlbVUjP9UNV+mWwD5MlM/Mtsq2azSiGM5bUMMj4QssxsodyamEwC 116 | W/POuZ6lcg5Ktz885hZo+L7tdEy8W9ViH0Pd 117 | -----END CERTIFICATE----- 118 | --------------------------------------------------------------------------------