Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Verifiable Credential Issuance and Verifiable Presentation Consumption #68

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
Basic Credential Issuance for Demo Credential
  • Loading branch information
bellebaum committed Sep 21, 2022

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
commit a8ab0e34fec873e69c86affdcf38e02b97c7c7d7
8 changes: 4 additions & 4 deletions lib/keys.rb
Original file line number Diff line number Diff line change
@@ -68,15 +68,15 @@ def self.store_key(bind)

# Certificates
if key_material['certs'].nil?
File.delete "#{filename}.cert" if File.exist? "#{filename}.cert"
FileUtils.rm_rf "#{filename}.cert"
else
pem = key_material['certs'].map(&:to_pem).join("\n")
File.write("#{filename}.cert", pem)
end

# Keys
if key_material['sk'].nil?
File.delete "#{filename}.key" if File.exist? "#{filename}.key"
FileUtils.rm_rf "#{filename}.key"
else
File.write("#{filename}.key", key_material['sk'])
end
@@ -107,8 +107,8 @@ def self.load_key(bind)
raise 'Certificate not yet valid' if certs[0].not_before > Time.now

result['certs'] = certs if result['sk'].nil? || (certs[0].check_private_key result['sk'])
rescue StandardError
p 'Loading certificate failed'
rescue StandardError => e
p "Loading certificate failed: #{e}"
end
end
result
103 changes: 103 additions & 0 deletions plugins/credential_issuance/credential_issuance.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# frozen_string_literal: true

require_relative 'id_credential'

# Cache for nonces
class NonceCache
class << self; attr_accessor :acceptable_nonces end
@acceptable_nonces = {} # Mapping from client_ids to nonces

def self.get_nonce(client_id)
nonce = SecureRandom.uuid
(@acceptable_nonces[client_id] ||= []) << nonce
nonce
end

def self.verify_nonce(client_id, nonce)
@acceptable_nonces[client_id]&.delete(nonce)
end
end

# Credential issuance endpoint
endpoint '/credential_issuance', ['POST'], public_endpoint: true do
token = Token.decode env.fetch('HTTP_AUTHORIZATION', '')&.slice(7..-1), nil
client = Client.find_by_id token['client_id']
user = User.find_by_id token['sub']
json = JSON.parse request.body.read
raise 'no_user_or_client' unless user && client
raise 'no_type_specified' unless json['type']
# Determine using the scopes whether a credential may be issued
raise 'insufficient_scope' unless token['scope'].split.include? "credential:#{json['type']}"

# Optionally verify PoP
if json['proof']
id = verify_identifier json['proof'], client
raise 'unaccepted_proof' unless id
end

# Build credential
credential = build_credential json['type'], json['format'], user, id
raise 'issuing_failed' unless credential&.dig('format') && credential&.dig('credential')

halt 200, { 'Content-Type' => 'application/json' }, credential.to_json
rescue StandardError => e
p e if debug
c_nonce = NonceCache.get_nonce client.client_id if client
halt 400, { 'Content-Type' => 'application/json' }, {
error: e.to_s,
c_nonce: c_nonce,
c_nonce_expires_in: 86_400
}.compact.to_json
end

# Verifies control over a cryptographic secret, whose public counterpart may be
# resolvable from an identifier (e.g. DID) or included in the proof (e.g. JWT).
# Consumes a nonce
def verify_identifier(pop, client)
return unless pop&.dig('proof_type')

case pop['proof_type']
when 'jwt'
verify_options = {
algorithms: %w[RS256 RS512 ES256 ES512],
verify_iat: true,
iss: client.client_id,
verify_iss: true,
aud: Config.base_config['issuer'],
verify_aud: true
}
body, header = JWT.decode pop['jwt'], nil, true, verify_options do |header, _body|
# We only support JWKs atm. TODO: Support for x5c
JWT::JWK.import(header['jwk']).keypair.public_key
end
# check nonce
return unless NonceCache.verify_nonce client.client_id, body['nonce']

jwk_thumbprint header['jwk']
end
end

# Temporary helper, until JWT can do this for us properly
def jwk_thumbprint(jwk)
jwk = jwk.clone
jwk.delete(:kid)
digest = Digest::SHA256.new
digest << jwk.sort.to_h.to_json
digest.base64digest.gsub('+', '-').gsub('/', '_').gsub('=', '')
end

# Calls other plugins to build a credential
def build_credential(type, format, subject, subject_id)
(PluginLoader.fire "PLUGIN_CREDENTIAL_ISSUANCE_BUILD_#{type.upcase}", binding).compact.first
end

# Adds the necessary data to the metadata
# Credentials are defined by other plugins
def add_to_metadata(bind)
metadata = bind.local_variable_get :metadata
metadata['credential_endpoint'] = "#{Config.base_config['front_url']}/credential_issuance"
credentials_supported = {}
PluginLoader.fire 'PLUGIN_CREDENTIAL_ISSUANCE_LIST', binding
metadata['credentials_supported'] = credentials_supported
end
PluginLoader.register 'STATIC_METADATA', method(:add_to_metadata)
62 changes: 62 additions & 0 deletions plugins/credential_issuance/id_credential.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# frozen_string_literal: true

def build_id_credential(bind)
req_format = bind.local_variable_get :format
subject = bind.local_variable_get :subject
subject_id = bind.local_variable_get :subject_id

# Require binding to an identifier
return unless subject_id

# We only support `vc_jwt`
return unless req_format == 'jwt_vc'

# Our ID Credential consists of a Name and birth date,
# and we only issue the credential if we have at least the following
return unless (subject.claim? 'given_name') && (subject.claim? 'family_name') && (subject.claim? 'birthdate')

credential_subject = { name: {} }
subject.attributes.each do |a|
credential_subject[:name][a['key']] = a['value'] if %w[given_name middle_name family_name].include? a['key']
credential_subject[a['key']] = a['value'] if a['key'] == 'birthdate'
end

# Assemble the JWT-VC
base_config = Config.base_config
now = Time.new.to_i
jwt_body = {
'iss' => base_config['issuer'],
'sub' => subject_id,
'jti' => SecureRandom.uuid,
'nbf' => now,
'iat' => now,
'exp' => now + (3600 * 24 * 365),
'nonce' => SecureRandom.uuid,
'vc' => {
'@context' => ['https://www.w3.org/2018/credentials/v1'],
'type' => %w[VerifiableCredential IDCredential],
'credentialSubject' => credential_subject
}
}
key_pair = Keys.load_key KEYS_TARGET_OMEJDN, 'omejdn', create_key: true
credential = JWT.encode jwt_body, key_pair['sk'], 'RS256', { typ: 'at+jwt', kid: key_pair['kid'] }
{ 'format' => 'jwt_vc', 'credential' => credential }
end
PluginLoader.register 'PLUGIN_CREDENTIAL_ISSUANCE_BUILD_ID_CREDENTIAL', method(:build_id_credential)

def id_credential_metadata(bind)
credentials = bind.local_variable_get :credentials_supported
credentials['id_credential'] = {
display: {
name: 'ID Credential'
},
formats: {
'jwt_vc' => {
'types' => %w[VerifiableCredential IDCredential],
'cryptographic_binding_methods_supported' => ['jwk'],
'cryptographic_suites_supported' => %w[RS256 RS512 ES256 ES512]
}
}
}
end
PluginLoader.register 'PLUGIN_CREDENTIAL_ISSUANCE_LIST', method(:id_credential_metadata)