pax_global_header00006660000000000000000000000064136224070000014504gustar00rootroot0000000000000052 comment=402b4019db8488e6924076da163560ec9ae359cb android_key_attestation-0.3.0/000077500000000000000000000000001362240700000164135ustar00rootroot00000000000000android_key_attestation-0.3.0/.gitignore000066400000000000000000000001611362240700000204010ustar00rootroot00000000000000/.bundle/ /.yardoc /_yardoc/ /coverage/ /doc/ /pkg/ /spec/reports/ /tmp/ # rspec failure tracking .rspec_status android_key_attestation-0.3.0/.rspec000066400000000000000000000000651362240700000175310ustar00rootroot00000000000000--format documentation --color --require spec_helper android_key_attestation-0.3.0/.rubocop.yml000066400000000000000000000051531362240700000206710ustar00rootroot00000000000000inherit_mode: merge: - AllowedNames AllCops: TargetRubyVersion: 2.3 DisabledByDefault: true Exclude: - "gemfiles/**/*" - "vendor/**/*" Bundler: Enabled: true Gemspec: Enabled: true Layout: Enabled: true Lint: Enabled: true Metrics/LineLength: Max: 120 Naming: Enabled: true Security: Enabled: true Style/BlockComments: Enabled: true Style/BracesAroundHashParameters: Enabled: true Style/CaseEquality: Enabled: true Style/ClassAndModuleChildren: Enabled: true Style/ClassMethods: Enabled: true Style/ClassVars: Enabled: true Style/CommentAnnotation: Enabled: true Style/ConditionalAssignment: Enabled: true Style/DefWithParentheses: Enabled: true Style/Dir: Enabled: true Style/EachForSimpleLoop: Enabled: true Style/EachWithObject: Enabled: true Style/EmptyBlockParameter: Enabled: true Style/EmptyCaseCondition: Enabled: true Style/EmptyElse: Enabled: true Style/EmptyLambdaParameter: Enabled: true Style/EmptyLiteral: Enabled: true Style/EvenOdd: Enabled: true Style/ExpandPathArguments: Enabled: true Style/For: Enabled: true Style/FrozenStringLiteralComment: Enabled: true Style/GlobalVars: Enabled: true Style/HashSyntax: Enabled: true Style/IdenticalConditionalBranches: Enabled: true Style/IfInsideElse: Enabled: true Style/InverseMethods: Enabled: true Style/MethodCallWithoutArgsParentheses: Enabled: true Style/MethodDefParentheses: Enabled: true Style/MultilineMemoization: Enabled: true Style/MutableConstant: Enabled: true Style/NestedParenthesizedCalls: Enabled: true Style/OptionalArguments: Enabled: true Style/ParenthesesAroundCondition: Enabled: true Style/RedundantBegin: Enabled: true Style/RedundantConditional: Enabled: true Style/RedundantException: Enabled: true Style/RedundantFreeze: Enabled: true Style/RedundantParentheses: Enabled: true Style/RedundantReturn: Enabled: true Style/RedundantSelf: Enabled: true Style/Semicolon: Enabled: true Style/SingleLineMethods: Enabled: true Style/SpecialGlobalVars: Enabled: true Style/SymbolLiteral: Enabled: true Style/StringLiterals: Enabled: true EnforcedStyle: double_quotes Style/TrailingBodyOnClass: Enabled: true Style/TrailingBodyOnMethodDefinition: Enabled: true Style/TrailingBodyOnModule: Enabled: true Style/TrailingMethodEndStatement: Enabled: true Style/TrivialAccessors: Enabled: true Style/UnneededInterpolation: Enabled: true Style/UnneededPercentQ: Enabled: true Style/UnpackFirst: Enabled: true Style/YodaCondition: Enabled: true Style/ZeroLengthPredicate: Enabled: true android_key_attestation-0.3.0/.travis.yml000066400000000000000000000004311362240700000205220ustar00rootroot00000000000000--- language: ruby cache: bundler rvm: - 2.6.5 - 2.5.7 - 2.4.9 - 2.3.8 before_install: - gem install bundler script: - bin/rspec jobs: fast_finish: true include: - rvm: 2.6.5 name: Rubocop script: - bundle info rubocop - bin/rubocop android_key_attestation-0.3.0/CHANGELOG.md000066400000000000000000000020621362240700000202240ustar00rootroot00000000000000# Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] ## [0.3.0] - 2020-02-16 ### Added - `Statement#verify_certificate_chain` to verify if the attestation certificate is trustworthy ## [0.2.0] - 2019-12-31 ### Changed - Raise `ChallengeMismatchError` if the challenge lengths are different, not `ArgumentError` ## [0.1.0] - 2019-12-29 ### Added - Extracted from [webauthn-ruby](https://github.com/cedarcode/webauthn-ruby) after discussion with the maintainers. Thanks for the feedback @grzuy and @brauliomartinezlm! [Unreleased]: https://github.com/bdewater/android_key_attestation/compare/v0.3.0...HEAD [0.3.0]: https://github.com/bdewater/android_key_attestation/compare/v0.2.0...v0.3.0 [0.2.0]: https://github.com/bdewater/android_key_attestation/compare/v0.1.0...v0.2.0 [0.1.0]: https://github.com/bdewater/android_key_attestation/releases/tag/v0.1.0 android_key_attestation-0.3.0/CODE_OF_CONDUCT.md000066400000000000000000000062351362240700000212200ustar00rootroot00000000000000# Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at bartdewater@gmail.com. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] [homepage]: http://contributor-covenant.org [version]: http://contributor-covenant.org/version/1/4/ android_key_attestation-0.3.0/Gemfile000066400000000000000000000002131362240700000177020ustar00rootroot00000000000000# frozen_string_literal: true source "https://rubygems.org" # Specify your gem's dependencies in android_key_attestation.gemspec gemspec android_key_attestation-0.3.0/Gemfile.lock000066400000000000000000000023371362240700000206420ustar00rootroot00000000000000PATH remote: . specs: android_key_attestation (0.3.0) GEM remote: https://rubygems.org/ specs: ast (2.4.0) byebug (11.0.1) coderay (1.1.2) diff-lcs (1.3) jaro_winkler (1.5.4) method_source (0.9.2) parallel (1.19.1) parser (2.6.5.0) ast (~> 2.4.0) pry (0.12.2) coderay (~> 1.1.0) method_source (~> 0.9.0) pry-byebug (3.7.0) byebug (~> 11.0) pry (~> 0.10) rainbow (3.0.0) rspec (3.9.0) rspec-core (~> 3.9.0) rspec-expectations (~> 3.9.0) rspec-mocks (~> 3.9.0) rspec-core (3.9.0) rspec-support (~> 3.9.0) rspec-expectations (3.9.0) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.9.0) rspec-mocks (3.9.0) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.9.0) rspec-support (3.9.0) rubocop (0.75.0) jaro_winkler (~> 1.5.1) parallel (~> 1.10) parser (>= 2.6) rainbow (>= 2.2.2, < 4.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 1.4.0, < 1.7) ruby-progressbar (1.10.1) unicode-display_width (1.6.0) PLATFORMS ruby DEPENDENCIES android_key_attestation! bundler pry-byebug rspec (~> 3.8) rubocop (= 0.75.0) BUNDLED WITH 1.17.3 android_key_attestation-0.3.0/README.md000066400000000000000000000051521362240700000176750ustar00rootroot00000000000000# AndroidKeyAttestation A Ruby gem to verify Android Key attestation statements on your server. Key attestation allows you to verify that the cryptographic keys you use in apps are stored the a hardware keystore of an Android device. ## Installation Add this line to your application's Gemfile: ```ruby gem 'android_key_attestation' ``` And then execute: $ bundle install Or install it yourself as: $ gem install android_key_attestation ## Usage Request an attestation statement as described in the [Android developer documentation](https://developer.android.com/training/articles/security-key-attestation#verifying) and send the certificate chain to your server application. In your server application code, do the following: ```ruby require "android_key_attestation" statement = AndroidKeyAttestation::Statement.new(certificates) # Verify the attestation certificate was issued for the challenge you generated begin statement.verify(challenge) rescue AndroidKeyAttestation::ChallengeMismatchError => e # abort end # Inspect properties of the attestation certificate belonging to the generated key pair, see # https://developer.android.com/training/articles/security-key-attestation#certificate_schema_keydescription # for more details. The gem uses snake_case versions of the lowerCamelCase names in the above link. statement.attestation_version # => 3 statement.attestation_security_level # => :trusted_environment statement.tee_enforced.purpose # => [:sign, :verify] statement.tee_enforced.origin # => :generated statement.software_enforced.creation_date # => 2018-07-29 08:31:54 -0400 ``` ## Development After checking out the repo, run `bin/setup` to install dependencies. Then, run `bin/rspec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). ## Contributing Bug reports and pull requests are welcome on GitHub at https://github.com/bdewater/android_key_attestation. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct. ## License The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). The gem and its authors are unaffiliated with Google. android_key_attestation-0.3.0/Rakefile000066400000000000000000000002211362240700000200530ustar00rootroot00000000000000# frozen_string_literal: true require "bundler/gem_tasks" require "rspec/core/rake_task" RSpec::Core::RakeTask.new(:spec) task default: :spec android_key_attestation-0.3.0/android_key_attestation.gemspec000066400000000000000000000024771362240700000247010ustar00rootroot00000000000000# frozen_string_literal: true require_relative "lib/android_key_attestation/version" Gem::Specification.new do |spec| spec.name = "android_key_attestation" spec.version = AndroidKeyAttestation::VERSION spec.authors = ["Bart de Water"] spec.summary = "Android key attestation verification" spec.homepage = "https://github.com/bdewater/android_key_attestation" spec.license = "MIT" if spec.respond_to?(:metadata) spec.metadata["homepage_uri"] = spec.homepage spec.metadata["source_code_uri"] = spec.homepage spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/master/CHANGELOG.md" end # Specify which files should be added to the gem when it is released. # The `git ls-files -z` loads the files in the RubyGem that have been added into git. spec.files = Dir.chdir(File.expand_path(__dir__)) do `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } end spec.bindir = "exe" spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } spec.require_paths = ["lib"] spec.required_ruby_version = ">= 2.3" spec.add_development_dependency "bundler" spec.add_development_dependency "pry-byebug" spec.add_development_dependency "rspec", "~> 3.8" spec.add_development_dependency "rubocop", "0.75.0" end android_key_attestation-0.3.0/bin/000077500000000000000000000000001362240700000171635ustar00rootroot00000000000000android_key_attestation-0.3.0/bin/console000077500000000000000000000006041362240700000205530ustar00rootroot00000000000000#!/usr/bin/env ruby # frozen_string_literal: true require "bundler/setup" require "android_key_attestation" # You can add fixtures and/or initialization code here to make experimenting # with your gem easier. You can also use a different console, if you like. # (If you use this, don't forget to add pry to your Gemfile!) # require "pry" # Pry.start require "irb" IRB.start(__FILE__) android_key_attestation-0.3.0/bin/rspec000077500000000000000000000015001362240700000202210ustar00rootroot00000000000000#!/usr/bin/env ruby # frozen_string_literal: true # # This file was generated by Bundler. # # The application 'rspec' is installed as part of a gem, and # this file is here to facilitate running it. # require "pathname" ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", Pathname.new(__FILE__).realpath) bundle_binstub = File.expand_path("bundle", __dir__) if File.file?(bundle_binstub) if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/ load(bundle_binstub) else abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") end end require "rubygems" require "bundler/setup" load Gem.bin_path("rspec-core", "rspec") android_key_attestation-0.3.0/bin/rubocop000077500000000000000000000015011362240700000205570ustar00rootroot00000000000000#!/usr/bin/env ruby # frozen_string_literal: true # # This file was generated by Bundler. # # The application 'rubocop' is installed as part of a gem, and # this file is here to facilitate running it. # require "pathname" ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", Pathname.new(__FILE__).realpath) bundle_binstub = File.expand_path("bundle", __dir__) if File.file?(bundle_binstub) if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/ load(bundle_binstub) else abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") end end require "rubygems" require "bundler/setup" load Gem.bin_path("rubocop", "rubocop") android_key_attestation-0.3.0/bin/setup000077500000000000000000000002031362240700000202440ustar00rootroot00000000000000#!/usr/bin/env bash set -euo pipefail IFS=$'\n\t' set -vx bundle install # Do any other automated setup that you need to do here android_key_attestation-0.3.0/lib/000077500000000000000000000000001362240700000171615ustar00rootroot00000000000000android_key_attestation-0.3.0/lib/android_key_attestation.rb000066400000000000000000000005751362240700000244240ustar00rootroot00000000000000# frozen_string_literal: true module AndroidKeyAttestation class Error < StandardError; end class ExtensionMissingError < Error; end class ChallengeMismatchError < Error; end class CertificateVerificationError < Error; end GEM_ROOT = File.expand_path(__dir__) end require_relative "android_key_attestation/statement" require_relative "android_key_attestation/version" android_key_attestation-0.3.0/lib/android_key_attestation/000077500000000000000000000000001362240700000240705ustar00rootroot00000000000000android_key_attestation-0.3.0/lib/android_key_attestation/authorization_list.rb000066400000000000000000000052311362240700000303510ustar00rootroot00000000000000# frozen_string_literal: true module AndroidKeyAttestation class AuthorizationList # https://source.android.com/security/keystore/attestation#attestation-extension PURPOSE_TAG = 1 ALGORITHM_TAG = 2 KEY_SIZE_TAG = 3 DIGEST_TAG = 5 PADDING_TAG = 6 EC_CURVE_TAG = 10 RSA_PUBLIC_EXPONENT_TAG = 200 ROLLBACK_RESISTANCE_TAG = 303 ACTIVE_DATE_TIME_TAG = 400 ORIGINATION_EXPIRE_DATE_TIME_TAG = 401 USAGE_EXPIRE_DATE_TIME_TAG = 402 NO_AUTH_REQUIRED_TAG = 503 USER_AUTH_TYPE_TAG = 504 AUTH_TIMEOUT_TAG = 505 ALLOW_WHILE_ON_BODY_TAG = 506 TRUSTED_USER_PRESENCE_REQUIRED_TAG = 507 TRUSTED_CONFIRMATION_REQUIRED_TAG = 508 UNLOCK_DEVICE_REQUIRED_TAG = 509 ALL_APPLICATIONS_TAG = 600 APPLICATION_ID_TAG = 601 CREATION_DATE_TIME_TAG = 701 ORIGIN_TAG = 702 ROOT_OF_TRUST_TAG = 704 OS_VERSION_TAG = 705 OS_PATCH_LEVEL_TAG = 706 ATTESTATION_APPLICATION_ID_TAG = 709 ATTESTATION_ID_BRAND_TAG = 710 ATTESTATION_ID_DEVICE_TAG = 711 ATTESTATION_ID_PRODUCT_TAG = 712 ATTESTATION_ID_SERIAL_TAG = 713 ATTESTATION_ID_IMEI_TAG = 714 ATTESTATION_ID_MEID_TAG = 715 ATTESTATION_ID_MANUFACTURER_TAG = 716 ATTESTATION_ID_MODEL_TAG = 717 VENDOR_PATCH_LEVEL_TAG = 718 BOOT_PATCH_LEVEL_TAG = 719 # https://source.android.com/security/keystore/tags PURPOSE_ENUM = { 0 => :encrypt, 1 => :decrypt, 2 => :sign, 3 => :verify, 4 => :derive_key, 5 => :wrap_key, }.freeze ORIGIN_ENUM = { 0 => :generated, 1 => :derived, 2 => :imported, 3 => :unknown, }.freeze def initialize(sequence) @sequence = sequence end def purpose integers = find_optional_integer_set(PURPOSE_TAG) integers&.map { |i| PURPOSE_ENUM.fetch(i) } end def all_applications find_boolean(ALL_APPLICATIONS_TAG) end def creation_date find_time_milliseconds(CREATION_DATE_TIME_TAG) end def origin integer = find_optional_integer(ORIGIN_TAG) ORIGIN_ENUM.fetch(integer) if integer end def find_by_tag(tag) sequence.detect { |data| data.tag == tag } end private attr_reader :sequence def find_optional_integer_set(tag) value = find_by_tag(tag)&.value&.at(0)&.value value&.map { |asn1_int| asn1_int.value.to_i } end def find_optional_integer(tag) find_by_tag(tag)&.value&.at(0)&.value&.to_i end def find_boolean(tag) find_by_tag(tag)&.value || false end def find_time_milliseconds(tag) value = find_by_tag(tag)&.value&.at(0)&.value Time.at(value.to_i / 1000) if value end end end android_key_attestation-0.3.0/lib/android_key_attestation/fixed_length_secure_compare.rb000066400000000000000000000011651362240700000321340ustar00rootroot00000000000000# frozen_string_literal: true require "openssl" module AndroidKeyAttestation module FixedLengthSecureCompare unless OpenSSL.singleton_class.method_defined?(:fixed_length_secure_compare) refine OpenSSL.singleton_class do def fixed_length_secure_compare(a, b) # rubocop:disable Naming/UncommunicativeMethodParamName raise ArgumentError, "inputs must be of equal length" unless a.bytesize == b.bytesize # borrowed from Rack::Utils l = a.unpack("C*") r, i = 0, -1 b.each_byte { |v| r |= v ^ l[i += 1] } r == 0 end end end end end android_key_attestation-0.3.0/lib/android_key_attestation/google_hardware_attestation_root.pem000066400000000000000000000036031362240700000334100ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIIFYDCCA0igAwIBAgIJAOj6GWMU0voYMA0GCSqGSIb3DQEBCwUAMBsxGTAXBgNV BAUTEGY5MjAwOWU4NTNiNmIwNDUwHhcNMTYwNTI2MTYyODUyWhcNMjYwNTI0MTYy ODUyWjAbMRkwFwYDVQQFExBmOTIwMDllODUzYjZiMDQ1MIICIjANBgkqhkiG9w0B AQEFAAOCAg8AMIICCgKCAgEAr7bHgiuxpwHsK7Qui8xUFmOr75gvMsd/dTEDDJdS Sxtf6An7xyqpRR90PL2abxM1dEqlXnf2tqw1Ne4Xwl5jlRfdnJLmN0pTy/4lj4/7 tv0Sk3iiKkypnEUtR6WfMgH0QZfKHM1+di+y9TFRtv6y//0rb+T+W8a9nsNL/ggj nar86461qO0rOs2cXjp3kOG1FEJ5MVmFmBGtnrKpa73XpXyTqRxB/M0n1n/W9nGq C4FSYa04T6N5RIZGBN2z2MT5IKGbFlbC8UrW0DxW7AYImQQcHtGl/m00QLVWutHQ oVJYnFPlXTcHYvASLu+RhhsbDmxMgJJ0mcDpvsC4PjvB+TxywElgS70vE0XmLD+O JtvsBslHZvPBKCOdT0MS+tgSOIfga+z1Z1g7+DVagf7quvmag8jfPioyKvxnK/Eg sTUVi2ghzq8wm27ud/mIM7AY2qEORR8Go3TVB4HzWQgpZrt3i5MIlCaY504LzSRi igHCzAPlHws+W0rB5N+er5/2pJKnfBSDiCiFAVtCLOZ7gLiMm0jhO2B6tUXHI/+M RPjy02i59lINMRRev56GKtcd9qO/0kUJWdZTdA2XoS82ixPvZtXQpUpuL12ab+9E aDK8Z4RHJYYfCT3Q5vNAXaiWQ+8PTWm2QgBR/bkwSWc+NpUFgNPN9PvQi8WEg5Um AGMCAwEAAaOBpjCBozAdBgNVHQ4EFgQUNmHhAHyIBQlRi0RsR/8aTMnqTxIwHwYD VR0jBBgwFoAUNmHhAHyIBQlRi0RsR/8aTMnqTxIwDwYDVR0TAQH/BAUwAwEB/zAO BgNVHQ8BAf8EBAMCAYYwQAYDVR0fBDkwNzA1oDOgMYYvaHR0cHM6Ly9hbmRyb2lk Lmdvb2dsZWFwaXMuY29tL2F0dGVzdGF0aW9uL2NybC8wDQYJKoZIhvcNAQELBQAD ggIBACDIw41L3KlXG0aMiS//cqrG+EShHUGo8HNsw30W1kJtjn6UBwRM6jnmiwfB Pb8VA91chb2vssAtX2zbTvqBJ9+LBPGCdw/E53Rbf86qhxKaiAHOjpvAy5Y3m00m qC0w/Zwvju1twb4vhLaJ5NkUJYsUS7rmJKHHBnETLi8GFqiEsqTWpG/6ibYCv7rY DBJDcR9W62BW9jfIoBQcxUCUJouMPH25lLNcDc1ssqvC2v7iUgI9LeoM1sNovqPm QUiG9rHli1vXxzCyaMTjwftkJLkf6724DFhuKug2jITV0QkXvaJWF4nUaHOTNA4u JU9WDvZLI1j83A+/xnAJUucIv/zGJ1AMH2boHqF8CY16LpsYgBt6tKxxWH00XcyD CdW2KlBCeqbQPcsFmWyWugxdcekhYsAWyoSf818NUsZdBWBaR/OukXrNLfkQ79Iy ZohZbvabO/X+MVT3rriAoKc8oE2Uws6DF+60PV7/WIPjNvXySdqspImSN78mflxD qwLqRBYkA3I75qppLGG9rp7UCdRjxMl8ZDBld+7yvHVgt1cVzJx9xnyGCC23Uaic MDSXYrB4I4WHXPGjxhZuCuPBLTdOLU8YRvMYdEvYebWHMpvwGCF6bAx3JBpIeOQ1 wDB5y0USicV3YgYGmi+NZfhA4URSh77Yd6uuJOJENRaNVTzk -----END CERTIFICATE----- android_key_attestation-0.3.0/lib/android_key_attestation/key_description.rb000066400000000000000000000030331362240700000276070ustar00rootroot00000000000000# frozen_string_literal: true require_relative "authorization_list" module AndroidKeyAttestation class KeyDescription # https://developer.android.com/training/articles/security-key-attestation#certificate_schema ATTESTATION_VERSION_INDEX = 0 ATTESTATION_SECURITY_LEVEL_INDEX = 1 KEYMASTER_VERSION_INDEX = 2 KEYMASTER_SECURITY_LEVEL_INDEX = 3 ATTESTATION_CHALLENGE_INDEX = 4 UNIQUE_ID_INDEX = 5 SOFTWARE_ENFORCED_INDEX = 6 TEE_ENFORCED_INDEX = 7 SECURITY_LEVEL_ENUM = { 0 => :software, 1 => :trusted_environment, 2 => :strong_box }.freeze def initialize(sequence) @sequence = sequence end def attestation_version Integer(sequence[ATTESTATION_VERSION_INDEX].value) end def attestation_security_level SECURITY_LEVEL_ENUM.fetch(Integer(sequence[ATTESTATION_SECURITY_LEVEL_INDEX].value)) end def keymaster_version Integer(sequence[KEYMASTER_VERSION_INDEX].value) end def keymaster_security_level SECURITY_LEVEL_ENUM.fetch(Integer(sequence[KEYMASTER_SECURITY_LEVEL_INDEX].value)) end def attestation_challenge sequence[ATTESTATION_CHALLENGE_INDEX].value end def unique_id sequence[UNIQUE_ID_INDEX].value end def tee_enforced @tee_enforced ||= AuthorizationList.new(sequence[TEE_ENFORCED_INDEX].value) end def software_enforced @software_enforced ||= AuthorizationList.new(sequence[SOFTWARE_ENFORCED_INDEX].value) end private attr_reader :sequence end end android_key_attestation-0.3.0/lib/android_key_attestation/statement.rb000066400000000000000000000036441362240700000264300ustar00rootroot00000000000000# frozen_string_literal: true require "forwardable" require "openssl" require_relative "key_description" require_relative "fixed_length_secure_compare" module AndroidKeyAttestation class Statement EXTENSION_DATA_OID = "1.3.6.1.4.1.11129.2.1.17" GOOGLE_ROOT_CERTIFICATES = begin file = File.read(File.join(GEM_ROOT, "android_key_attestation", "google_hardware_attestation_root.pem")) [OpenSSL::X509::Certificate.new(file)] end.freeze extend Forwardable def_delegators :key_description, :attestation_version, :attestation_security_level, :keymaster_version, :keymaster_security_level, :unique_id, :tee_enforced, :software_enforced using FixedLengthSecureCompare def initialize(*certificates) @certificates = certificates end def attestation_certificate @certificates.first end def verify_challenge(challenge) attestation_challenge = key_description.attestation_challenge attestation_challenge.bytesize == challenge.bytesize && OpenSSL.fixed_length_secure_compare(attestation_challenge, challenge) || raise(ChallengeMismatchError) end def verify_certificate_chain(root_certificates: GOOGLE_ROOT_CERTIFICATES, time: Time.now) store = OpenSSL::X509::Store.new root_certificates.each { |cert| store.add_cert(cert) } store.time = time store.verify(attestation_certificate, @certificates[1..-1]) || raise(CertificateVerificationError, store.error_string) end def key_description @key_description ||= begin extension_data = attestation_certificate.extensions.detect { |ext| ext.oid == EXTENSION_DATA_OID } raise AndroidKeyAttestation::ExtensionMissingError unless extension_data raw_key_description = OpenSSL::ASN1.decode(extension_data).value.last KeyDescription.new(OpenSSL::ASN1.decode(raw_key_description.value).value) end end end end android_key_attestation-0.3.0/lib/android_key_attestation/version.rb000066400000000000000000000001241362240700000260770ustar00rootroot00000000000000# frozen_string_literal: true module AndroidKeyAttestation VERSION = "0.3.0" end android_key_attestation-0.3.0/spec/000077500000000000000000000000001362240700000173455ustar00rootroot00000000000000android_key_attestation-0.3.0/spec/google_software_attestation_intermediate.pem000066400000000000000000000016241362240700000304120ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIICeDCCAh6gAwIBAgICEAEwCgYIKoZIzj0EAwIwgZgxCzAJBgNVBAYTAlVTMRMw EQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1Nb3VudGFpbiBWaWV3MRUwEwYD VQQKDAxHb29nbGUsIEluYy4xEDAOBgNVBAsMB0FuZHJvaWQxMzAxBgNVBAMMKkFu ZHJvaWQgS2V5c3RvcmUgU29mdHdhcmUgQXR0ZXN0YXRpb24gUm9vdDAeFw0xNjAx MTEwMDQ2MDlaFw0yNjAxMDgwMDQ2MDlaMIGIMQswCQYDVQQGEwJVUzETMBEGA1UE CAwKQ2FsaWZvcm5pYTEVMBMGA1UECgwMR29vZ2xlLCBJbmMuMRAwDgYDVQQLDAdB bmRyb2lkMTswOQYDVQQDDDJBbmRyb2lkIEtleXN0b3JlIFNvZnR3YXJlIEF0dGVz dGF0aW9uIEludGVybWVkaWF0ZTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABOue efhCY1msyyqRTImGzHCtkGaTgqlzJhP+rMv4ISdMIXSXSir+pblNf2bU4GUQZjW8 U7ego6ZxWD7bPhGuEBSjZjBkMB0GA1UdDgQWBBQ//KzWGrE6noEguNUlHMVlux6R qTAfBgNVHSMEGDAWgBTIrel3TEXDo88NFhDkeUM6IVowzzASBgNVHRMBAf8ECDAG AQH/AgEAMA4GA1UdDwEB/wQEAwIChDAKBggqhkjOPQQDAgNIADBFAiBLipt77oK8 wDOHri/AiZi03cONqycqRZ9pDMfDktQPjgIhAO7aAV229DLp1IQ7YkyUBO86fMy9 Xvsiu+f+uXc/WT/7 -----END CERTIFICATE----- android_key_attestation-0.3.0/spec/google_software_attestation_root.pem000066400000000000000000000016601362240700000267230ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIICizCCAjKgAwIBAgIJAKIFntEOQ1tXMAoGCCqGSM49BAMCMIGYMQswCQYDVQQG EwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNTW91bnRhaW4gVmll dzEVMBMGA1UECgwMR29vZ2xlLCBJbmMuMRAwDgYDVQQLDAdBbmRyb2lkMTMwMQYD VQQDDCpBbmRyb2lkIEtleXN0b3JlIFNvZnR3YXJlIEF0dGVzdGF0aW9uIFJvb3Qw HhcNMTYwMTExMDA0MzUwWhcNMzYwMTA2MDA0MzUwWjCBmDELMAkGA1UEBhMCVVMx EzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDU1vdW50YWluIFZpZXcxFTAT BgNVBAoMDEdvb2dsZSwgSW5jLjEQMA4GA1UECwwHQW5kcm9pZDEzMDEGA1UEAwwq QW5kcm9pZCBLZXlzdG9yZSBTb2Z0d2FyZSBBdHRlc3RhdGlvbiBSb290MFkwEwYH KoZIzj0CAQYIKoZIzj0DAQcDQgAE7l1ex+HA220Dpn7mthvsTWpdamguD/9/SQ59 dx9EIm29sa/6FsvHrcV30lacqrewLVQBXT5DKyqO107sSHVBpKNjMGEwHQYDVR0O BBYEFMit6XdMRcOjzw0WEOR5QzohWjDPMB8GA1UdIwQYMBaAFMit6XdMRcOjzw0W EOR5QzohWjDPMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgKEMAoGCCqG SM49BAMCA0cAMEQCIDUho++LNEYenNVg8x1YiSBq3KNlQfYNns6KGYxmSGB7AiBN C/NR2TB8fVvaNTQdqEcbY6WFZTytTySn502vQX3xvw== -----END CERTIFICATE----- android_key_attestation-0.3.0/spec/spec_helper.rb000066400000000000000000000011221362240700000221570ustar00rootroot00000000000000# frozen_string_literal: true require "bundler/setup" require "android_key_attestation" require "pry-byebug" RSpec.configure do |config| # Enable flags like --only-failures and --next-failure config.example_status_persistence_file_path = ".rspec_status" # Disable RSpec exposing methods globally on `Module` and `main` config.disable_monkey_patching! config.expect_with :rspec do |c| c.syntax = :expect end config.order = :random end def test_certificate @test_certificate ||= OpenSSL::X509::Certificate.new(File.read(File.join(__dir__, "test_certificate.pem"))) end android_key_attestation-0.3.0/spec/statement_spec.rb000066400000000000000000000065111362240700000227130ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe AndroidKeyAttestation::Statement do subject { described_class.new(test_certificate) } context "#attestation_certificate" do it "returns true the first certificate in the chain" do expect(subject.attestation_certificate.to_pem).to eq(test_certificate.to_pem) end end context "#verify_challenge" do it "returns true if the challenge matches" do expect(subject.verify_challenge("abc")).to be true end it "raises an error if the challenge does not match" do expect { subject.verify_challenge("foo") }.to raise_error(AndroidKeyAttestation::ChallengeMismatchError) end it "raises an error if the challenge is of different length" do expect { subject.verify_challenge("foobar") }.to raise_error(AndroidKeyAttestation::ChallengeMismatchError) end end context "#verify_certificate_chain" do subject { described_class.new(intermediate_certificate) } let(:spec_path) { File.join(AndroidKeyAttestation::GEM_ROOT, "..", "spec") } let(:root_certificate) do OpenSSL::X509::Certificate.new(File.read(File.join(spec_path, "google_software_attestation_root.pem"))) end let(:intermediate_certificate) do OpenSSL::X509::Certificate.new(File.read(File.join(spec_path, "google_software_attestation_intermediate.pem"))) end let(:time) { Time.utc(2019, 12, 31) } it "returns true if the chain is valid" do expect(subject.verify_certificate_chain(root_certificates: [root_certificate], time: time)).to be true end it "raises error if the chain is not valid" do expect { subject.verify_certificate_chain(time: time) }.to( raise_error(AndroidKeyAttestation::CertificateVerificationError) ) end end context "#key_description" do it "raises an error is the extension data is missing" do expect { described_class.new(OpenSSL::X509::Certificate.new).key_description }.to( raise_error(AndroidKeyAttestation::ExtensionMissingError) ) end end context "#attestation_version" do specify do expect(subject.attestation_version).to eq(3) end end context "#attestation_security_level" do specify do expect(subject.attestation_security_level).to eq(:trusted_environment) end end context "#keymaster_version" do specify do expect(subject.keymaster_version).to eq(4) end end context "#keymaster_security_level" do specify do expect(subject.keymaster_security_level).to eq(:trusted_environment) end end context "#unique_id" do specify do expect(subject.unique_id).to eq("") end end context "#tee_enforced" do subject { described_class.new(test_certificate).tee_enforced } context "#purpose" do specify do expect(subject.purpose).to match_array([:sign, :verify]) end end context "#origin" do specify do expect(subject.origin).to eq(:generated) end end end context "#software_enforced" do subject { described_class.new(test_certificate).software_enforced } context "#creation_date" do specify do expect(subject.creation_date).to eq(Time.utc(2018, 07, 29, 12, 31, 54)) end end context "#all_applications" do specify do expect(subject.all_applications).to be false end end end end android_key_attestation-0.3.0/spec/test_certificate.pem000066400000000000000000000041421362240700000233720ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIIGCDCCBHCgAwIBAgIBATANBgkqhkiG9w0BAQsFADApMRkwFwYDVQQFExAyZGM1OGIyZDFhMjQx MzI2MQwwCgYDVQQMDANURUUwIBcNNzAwMTAxMDAwMDAwWhgPMjEwNjAyMDcwNjI4MTVaMB8xHTAb BgNVBAMMFEFuZHJvaWQgS2V5c3RvcmUgS2V5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC AQEApNVcnyN40MANMbbo2nMGNq2NNysDSjfLm0W3i6wPKf0ffCYkhWM4dCmQKKf50uAZTBeTit4c NwXeZn3qellMlOsIN3Qc384rfN/8cikrRvUAgibz0Jy7STykjwa7x6tKwqITxbO8HqAhKo8/BQXU xzrOdIg5ciy+UM7Vgh7a7ogen0KL2iGgrsalb1ti7Vlzb6vIJ4WzIC3TGD2sCkoPahghwqFDZZCo /FzaLoNY0jAUX2mL+kf8aUaoxz7xA9FTvgara+1pLBR1s4c8xPS2HdZipcVXWfey0wujv1VAKs4+ tXjKlHkYBHBBceEjxUtEmrapSQEdpHPv7Xh9Uanq4QIDAQABo4ICwTCCAr0wDgYDVR0PAQH/BAQD AgeAMIICqQYKKwYBBAHWeQIBEQSCApkwggKVAgEDCgEBAgEECgEBBANhYmMEADCCAc2/hT0IAgYB ZOYGEYe/hUWCAbsEggG3MIIBszGCAYswDAQHYW5kcm9pZAIBHTAZBBRjb20uYW5kcm9pZC5rZXlj aGFpbgIBHTAZBBRjb20uYW5kcm9pZC5zZXR0aW5ncwIBHTAZBBRjb20ucXRpLmRpYWdzZXJ2aWNl cwIBHTAaBBVjb20uYW5kcm9pZC5keW5zeXN0ZW0CAR0wHQQYY29tLmFuZHJvaWQuaW5wdXRkZXZp Y2VzAgEdMB8EGmNvbS5hbmRyb2lkLmxvY2FsdHJhbnNwb3J0AgEdMB8EGmNvbS5hbmRyb2lkLmxv Y2F0aW9uLmZ1c2VkAgEdMB8EGmNvbS5hbmRyb2lkLnNlcnZlci50ZWxlY29tAgEdMCAEG2NvbS5h bmRyb2lkLndhbGxwYXBlcmJhY2t1cAIBHTAhBBxjb20uZ29vZ2xlLlNTUmVzdGFydERldGVjdG9y AgEdMCIEHWNvbS5nb29nbGUuYW5kcm9pZC5oaWRkZW5tZW51AgEBMCMEHmNvbS5hbmRyb2lkLnBy b3ZpZGVycy5zZXR0aW5ncwIBHTEiBCAwGqPLCBE0UBxF8UIqvGbCQiT9Xe1f3I8X5pcXb9hmqjCB rqEIMQYCAQICAQOiAwIBAaMEAgIIAKUFMQMCAQSmCDEGAgEDAgEFv4FIBQIDAQABv4N3AgUAv4U+ AwIBAL+FQEwwSgQgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQAKAQIEIHKNsSdP HxzxVx3kOAsEilVKxKOA529TVQg1KQhKk3gBv4VBAwIBAL+FQgUCAwMUs7+FTgUCAwMUs7+FTwUC AwMUszANBgkqhkiG9w0BAQsFAAOCAYEAJMIuzdNUdfrE6sIdmsnMn/scSG2odbphj8FkX9JGdF2S OT599HuDY9qhvkru2Dza4sLKK3f4ViBhuR9lpfeprKvstxbtBO7jkLYfVn0ZRzHRHVEyiW5IVKh+ qOXVJ9S1lMShOTlsaYJytLKIlcrRAZBEXZiNbzTuVh1CH6X9Ni1dog14snm+lcOeORdL9fht2CHa u/caRnpWiZbjoAoJp0O89uBrRkXPpln51+3jPY6AFny30grNAvKguauDcPPhNV1yR+ylSsQi2gm3 Rs4pgtlxFLMfZLgT0cbkl+9zk/QUqlpBP8ftUBsOI0ARr8xhFN3cvq9kXGLtJ9hEP9PRaflAFREk DK3IBIbVcAFZBFoAQOdE9zy0+F5bQrznPGaZg4Dzhcx33qMDUTgHtWoy+k3ePGQMEtmoTTLgQywW OIkXEoFqqGi9GKJXUT1KYi5NsigaYqu7FoN4Qsvs61pMUEfZSPP2AFwkA8uNFbmb9uxcxaGHCA8i 3i9VM6yOLIrP -----END CERTIFICATE-----