From d4ba1f8914efb26c0931e1409303a9c9c5a757b6 Mon Sep 17 00:00:00 2001 From: Niels van Dijk Date: Mon, 21 Jul 2025 16:09:37 +0200 Subject: [PATCH 1/3] Initial commit for json schema for SCHAC --- schema/vc/mkSchema.py | 137 ++++++++++++++++++++++++++++ schema/vc/schac_ldap.json | 186 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 323 insertions(+) create mode 100755 schema/vc/mkSchema.py create mode 100644 schema/vc/schac_ldap.json diff --git a/schema/vc/mkSchema.py b/schema/vc/mkSchema.py new file mode 100755 index 0000000..3455ef9 --- /dev/null +++ b/schema/vc/mkSchema.py @@ -0,0 +1,137 @@ +#! /bin/python3 + +import sys +import json +from pathlib import Path + + +def loadJSON(json_file): + with open(json_file) as json_file: + return json.load(json_file) + +def p(message, writetolog=False): + if writetolog: + write_log(message) + else: + print(message) + +def pj(the_json, writetolog=False): + p(json.dumps(the_json, indent=4, sort_keys=False), writetolog) + + +def write_file(contents, filepath, mkpath=True, overwrite=False, type='txt'): + if mkpath: + Path(filepath).mkdir(parents=True, exist_ok=overwrite) + + if overwrite: + f = open(filepath, "w") + else: + f = open(filepath, "a") + + match type: + case 'yaml': + yaml.preserve_quotes = True + yaml.dump(contents, f) + case 'json': + f.write(json.dumps(contents,sort_keys=False, indent=4, ensure_ascii=False,separators=(',', ':'))) + case _: + # assume text + f.write(contents+"\n") + + f.close() + +def replace_capitals(s): + return ''.join(['' + ("_" + char.lower()) if char.isupper() else char for char in s]).strip() + +def covert_schemaname(attributeName): + # Attributes need to be rename to claims + # as a general rule of thumb: + # + # eduPerson, voPerson and Schac schema names are just lowercased ad get added an underscore + # + # for the attrbute name: if a captical is found, it is replaced with a lower and prefixed with an underscore. + # + # sp eduPersonScopedAffiliation becomes: eduperson_scoped_affiliation + # + s= attributeName + + if s.startswith('eduPerson'): + return ("eduperson" + replace_capitals(s[9:]).replace("_i_d", "_id").replace("_d_n", "_dn")) + elif s.startswith('voPerson'): + return ("voperson" + replace_capitals(s[7:]).replace("_i_d", "_id").replace("_d_n", "_dn")) + elif s.startswith('Schac'): + return ("schac" + replace_capitals(s[5:]).replace("_i_d", "_id").replace("_d_n", "_dn")) + else: + return (replace_capitals(s).replace("_u_r_i", "_uri").replace("_s_m_i_m_e", "_smime")) + +def mapType(equality, multivalued = False): + + if multivalued: + match equality: + case "caseExactMatch" | "caseIgnoreMatch": + return "string" + case "integerMatch": + return "integer" + case "numericStringMatch": + return "number" + else: + return None + +def main(argv): + schemaFile = "schac_1_6_0.json" + + schema = { + "$id": "https://refeds.org/schemas/vc/schac_1_6_0.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$comment": "This schema implements the SCHAC schema version 1.6.0 (23 May 2022) - https://refeds.org/specifications/schac - This schema is maintained by REFEDs (https://refeds.org). The mission of REFEDS (the Research and Education FEDerations group) is to be the voice that articulates the mutual needs of research and education identity federations worldwide.", + "title": "SCHAC_credentials", + "description": "Schema for verifiable credential claims describing a user in the context of SCHAC", + "type": "object", + "properties": { + "credentialSubject": { + "type": "object", + "properties": {} + } + } + } + + # Generate object properties based on ldif schema,coverted to json by chatGTP + # The resulting schac_ldif.json was added for reference + # + schemaJSON = loadJSON('schac_ldap.json') + object_props = {} + att = schemaJSON['schema']['attributes'] + + for a in att: + name = covert_schemaname(a['name']) + + p(a['equality']) + p(('singleValue' in a)) + + object_props[name] = { + "type": mapType(a['equality'], ('singleValue' in a)), + "description": a['description'], + } + # handle enums + #if 'format' in a.keys(): + # object_props[name]['enum'] = a['format'] + + # TODO: validate these + # TODO: we still have several format and pattern definitions missing + match a['name']: + case "schacHomeOrganizationType" | "schacUserPresenceID" |"schacPersonalPosition" | "schacPersonalUniqueCode" : + object_props[name]['type'] = "array" + object_props[name]['anyof'] = { "type": "uri" } + + case "schacGender": + object_props[name]['type'] = "integer" + object_props[name]['deprecated'] = True + + schema["properties"]["credentialSubject"]["properties"] = object_props + + pj(schema) + + #write_file(schema, schemaFile, mkpath=False, overwrite=True, type='json') + +if __name__ == "__main__": + main(sys.argv[1:]) \ No newline at end of file diff --git a/schema/vc/schac_ldap.json b/schema/vc/schac_ldap.json new file mode 100644 index 0000000..92aa10b --- /dev/null +++ b/schema/vc/schac_ldap.json @@ -0,0 +1,186 @@ +{ + "schema": { + "version": "1.6.0", + "description": "SCHema for ACademia (SCHAC) Attribute definitions for individual data", + "objectIdentifiers": { + "GEANT": "1.3.6.1.4.1.25178", + "schac": "GEANT:1", + "schacExperimental": "schac:0", + "schacObjectClass": "schac:1", + "schacAttributeType": "schac:2", + "schacExpObjClass": "schacExperimental:1", + "schacExpAttr": "schacExperimental:2" + }, + "attributes": [ + { + "id": "schacAttributeType:1", + "name": "schacMotherTongue", + "description": "RFC 3066 code for preferred language of communication", + "equality": "caseExactMatch", + "singleValue": true, + "syntaxOID": "1.3.6.1.4.1.1466.115.121.1.15", + "examples": ["fr", "es-ES"] + }, + { + "id": "schacAttributeType:2", + "name": "schacGender", + "description": "Representation of human sex (see ISO 5218). Deprecated as of SCHAC 1.6.0", + "equality": "integerMatch", + "singleValue": true, + "syntaxOID": "1.3.6.1.4.1.1466.115.121.1.27", + "format": { + "0": "Not known", + "1": "Male", + "2": "Female", + "9": "Not specified" + }, + "example": "2" + }, + { + "id": "schacAttributeType:3", + "name": "schacDateOfBirth", + "description": "Date of birth (format YYYYMMDD, only numeric chars)", + "equality": "numericStringMatch", + "ordering": "numericStringOrderingMatch", + "substring": "numericStringSubstringsMatch", + "singleValue": true, + "syntaxOID": "1.3.6.1.4.1.1466.115.121.1.36", + "example": "19660412" + }, + { + "id": "schacAttributeType:4", + "name": "schacPlaceOfBirth", + "description": "Birth place of a person", + "equality": "caseIgnoreMatch", + "ordering": "caseIgnoreOrderingMatch", + "substring": "caseIgnoreSubstringsMatch", + "singleValue": true, + "syntaxOID": "1.3.6.1.4.1.1466.115.121.1.15", + "example": "Algeciras, Spain" + }, + { + "id": "schacAttributeType:5", + "name": "schacCountryOfCitizenship", + "description": "Country of citizenship of a person. Format two-letter acronym according to ISO 3166-1 alpha 2", + "equality": "caseIgnoreMatch", + "singleValue": true, + "syntaxOID": "1.3.6.1.4.1.1466.115.121.1.15", + "example": "es" + }, + { + "id": "schacAttributeType:6", + "name": "schacSn1", + "description": "First surname of a person", + "equality": "caseIgnoreMatch", + "ordering": "caseIgnoreOrderingMatch", + "substring": "caseIgnoreSubstringsMatch", + "singleValue": true, + "syntaxOID": "1.3.6.1.4.1.1466.115.121.1.15", + "examples": [ + "Lopez de la Moraleda", + "Wolniewicz" + ] + }, + { + "id": "schacAttributeType:7", + "name": "schacSn2", + "description": "Second surname of a person", + "equality": "caseIgnoreMatch", + "ordering": "caseIgnoreOrderingMatch", + "substring": "caseIgnoreSubstringsMatch", + "singleValue": true, + "syntaxOID": "1.3.6.1.4.1.1466.115.121.1.15", + "examples": [ + "de Las Altas Alcurnias", + "Gorecka" + ] + }, + { + "id": "schacAttributeType:8", + "name": "schacPersonalTitle", + "description": "RFC1274: personal title", + "equality": "caseIgnoreMatch", + "substring": "caseIgnoreSubstringsMatch", + "singleValue": true, + "syntaxOID": "1.3.6.1.4.1.1466.115.121.1.15", + "example": "Prof" + }, + { + "id": "schacAttributeType:9", + "name": "schacHomeOrganization", + "description": "Domain name of the home organization", + "equality": "caseIgnoreMatch", + "substring": "caseIgnoreSubstringsMatch", + "singleValue": true, + "syntaxOID": "1.3.6.1.4.1.1466.115.121.1.15", + "example": "tut.fi" + }, + { + "id": "schacAttributeType:10", + "name": "schacHomeOrganizationType", + "description": "Type of the home organization", + "equality": "caseIgnoreMatch", + "syntaxOID": "1.3.6.1.4.1.1466.115.121.1.15", + "examples": [ + "urn:schac.org:schac:homeOrganizationType:int:university", + "urn:schac.org:schac:homeOrganizationType:int:uas", + "urn:schac.org:schac:homeOrganizationType:int:research-institution", + "urn:schac.org:schac:homeOrganizationType:int:university-hospital", + "urn:schac.org:schac:homeOrganizationType:int:nren", + "urn:schac.org:schac:homeOrganizationType:int:other", + "urn:schac.org:schac:homeOrganizationType:ch:vho", + "urn:schac.org:schac:homeOrganizationType:es:opi" + ] + }, + { + "id": "schacAttributeType:11", + "name": "schacCountryOfResidence", + "description": "Country of residence of a person. Format two-letter acronym according to ISO 3166-1 alpha 2", + "equality": "caseIgnoreMatch", + "singleValue": true, + "syntaxOID": "1.3.6.1.4.1.1466.115.121.1.15", + "example": "es" + }, + { + "id": "schacAttributeType:12", + "name": "schacUserPresenceID", + "description": "Used to store a set of values related to the network presence", + "equality": "caseExactMatch", + "substring": "caseExactSubstringsMatch", + "syntaxOID": "1.3.6.1.4.1.1466.115.121.1.15", + "examples": [ + "xmpp:pepe@im.univx.es", + "sip:jose.perez@myweb.es", + "sip:+34-95-505-6600@univx.es;transport=TCP;user=phone", + "sips:alice@atlanta.com?subject=project%20x&priority=urgent", + "h323:pepe@myweb.fi:808;pars", + "skype:pepe.perez" + ] + }, + { + "id": "schacAttributeType:13", + "name": "schacPersonalPosition", + "description": "Position inside an institution", + "equality": "caseIgnoreMatch", + "substring": "caseIgnoreSubstringsMatch", + "syntaxOID": "1.3.6.1.4.1.1466.115.121.1.15", + "example": "urn:schac.org:schac:personalPosition:pl:umk.pl:programmer" + }, + { + "id": "schacAttributeType:14", + "name": "schacPersonalUniqueCode", + "description": "unique code for the subject", + "equality": "caseIgnoreMatch", + "ordering": "caseIgnoreOrderingMatch", + "substring": "caseIgnoreSubstringsMatch", + "syntaxOID": "1.3.6.1.4.1.1466.115.121.1.15", + "examples": [ + "urn:schac.org:schac:personalUniqueCode:int:studentID::", + "urn:schac.org:schac:personalUniqueCode:fi:tut.fi:hetu:010161-995A", + "urn:schac.org:schac:personalUniqueCode:es:uma:estudiante:a3b123c12", + "urn:schac.org:schac:personalUniqueCode:se:LIN:studentId:20001234" + ] + } + ] + } +} \ No newline at end of file From 618612f5098598693976045e68a3b78a6aeef027 Mon Sep 17 00:00:00 2001 From: Niels van Dijk Date: Tue, 22 Jul 2025 10:18:15 +0200 Subject: [PATCH 2/3] Updates --- schema/vc/mkSchema.py | 33 +++-- schema/vc/schac_1_6_0.json | 87 +++++++++++ schema/vc/schac_ldif.json | 288 +++++++++++++++++++++++++++++++++++++ 3 files changed, 391 insertions(+), 17 deletions(-) create mode 100644 schema/vc/schac_1_6_0.json create mode 100644 schema/vc/schac_ldif.json diff --git a/schema/vc/mkSchema.py b/schema/vc/mkSchema.py index 3455ef9..3767807 100755 --- a/schema/vc/mkSchema.py +++ b/schema/vc/mkSchema.py @@ -56,13 +56,16 @@ def covert_schemaname(attributeName): s= attributeName if s.startswith('eduPerson'): - return ("eduperson" + replace_capitals(s[9:]).replace("_i_d", "_id").replace("_d_n", "_dn")) + return ("eduperson" + fixName(replace_capitals(s[9:]))) elif s.startswith('voPerson'): - return ("voperson" + replace_capitals(s[7:]).replace("_i_d", "_id").replace("_d_n", "_dn")) + return ("voperson" + fixName(replace_capitals(s[7:]))) elif s.startswith('Schac'): - return ("schac" + replace_capitals(s[5:]).replace("_i_d", "_id").replace("_d_n", "_dn")) + return ("schac" + fixName(replace_capitals(s[5:]))) else: - return (replace_capitals(s).replace("_u_r_i", "_uri").replace("_s_m_i_m_e", "_smime")) + return (fixName(replace_capitals(s))) + +def fixName(name): + return name.replace("_i_d", "_id").replace("_d_n", "_dn").replace("_u_r_i", "_uri").replace("_s_m_i_m_e", "_smime") def mapType(equality, multivalued = False): @@ -83,8 +86,8 @@ def main(argv): schema = { "$id": "https://refeds.org/schemas/vc/schac_1_6_0.json", "$schema": "https://json-schema.org/draft/2020-12/schema", - "$comment": "This schema implements the SCHAC schema version 1.6.0 (23 May 2022) - https://refeds.org/specifications/schac - This schema is maintained by REFEDs (https://refeds.org). The mission of REFEDS (the Research and Education FEDerations group) is to be the voice that articulates the mutual needs of research and education identity federations worldwide.", - "title": "SCHAC_credentials", + "$comment": "This schema implements SCHAC (SCHema for ACademia) version 1.6.0 (23 May 2022) - https://refeds.org/specifications/schac - This schema is maintained by REFEDs (https://refeds.org). The mission of REFEDS (the Research and Education FEDerations group) is to be the voice that articulates the mutual needs of research and education identity federations worldwide.", + "title": "SCHACcredentials", "description": "Schema for verifiable credential claims describing a user in the context of SCHAC", "type": "object", "properties": { @@ -104,17 +107,10 @@ def main(argv): for a in att: name = covert_schemaname(a['name']) - - p(a['equality']) - p(('singleValue' in a)) - object_props[name] = { "type": mapType(a['equality'], ('singleValue' in a)), "description": a['description'], } - # handle enums - #if 'format' in a.keys(): - # object_props[name]['enum'] = a['format'] # TODO: validate these # TODO: we still have several format and pattern definitions missing @@ -127,11 +123,14 @@ def main(argv): object_props[name]['type'] = "integer" object_props[name]['deprecated'] = True - schema["properties"]["credentialSubject"]["properties"] = object_props + case "schacDateOfBirth": + object_props[name]['maxLength'] = 8 - pj(schema) - - #write_file(schema, schemaFile, mkpath=False, overwrite=True, type='json') + case "schacCountryOfCitizenship" | "schacCountryOfResidence": + object_props[name]['maxLength'] = 2 + + schema["properties"]["credentialSubject"]["properties"] = object_props + write_file(schema, schemaFile, mkpath=False, overwrite=True, type='json') if __name__ == "__main__": main(sys.argv[1:]) \ No newline at end of file diff --git a/schema/vc/schac_1_6_0.json b/schema/vc/schac_1_6_0.json new file mode 100644 index 0000000..f1b39b9 --- /dev/null +++ b/schema/vc/schac_1_6_0.json @@ -0,0 +1,87 @@ +{ + "$id":"https://refeds.org/schemas/vc/schac_1_6_0.json", + "$schema":"https://json-schema.org/draft/2020-12/schema", + "$comment":"This schema implements SCHAC (SCHema for ACademia) version 1.6.0 (23 May 2022) - https://refeds.org/specifications/schac - This schema is maintained by REFEDs (https://refeds.org). The mission of REFEDS (the Research and Education FEDerations group) is to be the voice that articulates the mutual needs of research and education identity federations worldwide.", + "title":"SCHACcredentials", + "description":"Schema for verifiable credential claims describing a user in the context of SCHAC", + "type":"object", + "properties":{ + "credentialSubject":{ + "type":"object", + "properties":{ + "schac_mother_tongue":{ + "type":"string", + "description":"RFC 3066 code for preferred language of communication" + }, + "schac_gender":{ + "type":"integer", + "description":"Representation of human sex (see ISO 5218). Deprecated as of SCHAC 1.6.0", + "deprecated":true + }, + "schac_date_of_birth":{ + "type":"number", + "description":"Date of birth (format YYYYMMDD, only numeric chars)", + "maxLength":8 + }, + "schac_place_of_birth":{ + "type":"string", + "description":"Birth place of a person" + }, + "schac_country_of_citizenship":{ + "type":"string", + "description":"Country of citizenship of a person. Format two-letter acronym according to ISO 3166-1 alpha 2", + "maxLength":2 + }, + "schac_sn1":{ + "type":"string", + "description":"First surname of a person" + }, + "schac_sn2":{ + "type":"string", + "description":"Second surname of a person" + }, + "schac_personal_title":{ + "type":"string", + "description":"RFC1274: personal title" + }, + "schac_home_organization":{ + "type":"string", + "description":"Domain name of the home organization" + }, + "schac_home_organization_type":{ + "type":"array", + "description":"Type of the home organization", + "anyof":{ + "type":"uri" + } + }, + "schac_country_of_residence":{ + "type":"string", + "description":"Country of residence of a person. Format two-letter acronym according to ISO 3166-1 alpha 2", + "maxLength":2 + }, + "schac_user_presence_id":{ + "type":"array", + "description":"Used to store a set of values related to the network presence", + "anyof":{ + "type":"uri" + } + }, + "schac_personal_position":{ + "type":"array", + "description":"Position inside an institution", + "anyof":{ + "type":"uri" + } + }, + "schac_personal_unique_code":{ + "type":"array", + "description":"unique code for the subject", + "anyof":{ + "type":"uri" + } + } + } + } + } +} \ No newline at end of file diff --git a/schema/vc/schac_ldif.json b/schema/vc/schac_ldif.json new file mode 100644 index 0000000..716a92f --- /dev/null +++ b/schema/vc/schac_ldif.json @@ -0,0 +1,288 @@ +{ + "dn": "cn=schac,cn=schema,cn=config", + "objectClass": "olcSchemaConfig", + "cn": "schac", + "olcObjectIdentifier": [ + "GEANT 1.3.6.1.4.1.25178", + "schac GEANT:1", + "schacExperimental schac:0", + "schacObjectClass schac:1", + "schacAttributeType schac:2", + "schacExpObjClass schacExperimental:1", + "schacExpAttr schacExperimental:2" + ], + "olcAttributeTypes": [ + { + "id": "schacAttributeType:1", + "NAME": "schacMotherTongue", + "DESC": "RFC 3066 code for preferred language of communication", + "EQUALITY": "caseExactMatch", + "SINGLE-VALUE": true, + "SYNTAX": "1.3.6.1.4.1.1466.115.121.1.15" + }, + { + "id": "schacAttributeType:2", + "NAME": "schacGender", + "DESC": "Deprecated as of SCHAC 1.6.0", + "EQUALITY": "integerMatch", + "SINGLE-VALUE": true, + "SYNTAX": "1.3.6.1.4.1.1466.115.121.1.27" + }, + { + "id": "schacAttributeType:3", + "NAME": "schacDateOfBirth", + "DESC": "Date of birth (format YYYYMMDD, only numeric chars)", + "EQUALITY": "numericStringMatch", + "ORDERING": "numericStringOrderingMatch", + "SUBSTR": "numericStringSubstringsMatch", + "SINGLE-VALUE": true, + "SYNTAX": "1.3.6.1.4.1.1466.115.121.1.36" + }, + { + "id": "schacAttributeType:4", + "NAME": "schacPlaceOfBirth", + "DESC": "Birth place of a person", + "EQUALITY": "caseIgnoreMatch", + "ORDERING": "caseIgnoreOrderingMatch", + "SUBSTR": "caseIgnoreSubstringsMatch", + "SINGLE-VALUE": true, + "SYNTAX": "1.3.6.1.4.1.1466.115.121.1.15" + }, + { + "id": "schacAttributeType:5", + "NAME": "schacCountryOfCitizenship", + "DESC": "Country of citizenship of a person. Format two-letter acronym according to ISO 3166-1 alpha 2", + "EQUALITY": "caseIgnoreMatch", + "SYNTAX": "1.3.6.1.4.1.1466.115.121.1.15" + }, + { + "id": "schacAttributeType:6", + "NAME": "schacSn1", + "DESC": "First surname of a person", + "EQUALITY": "caseIgnoreMatch", + "ORDERING": "caseIgnoreOrderingMatch", + "SUBSTR": "caseIgnoreSubstringsMatch", + "SYNTAX": "1.3.6.1.4.1.1466.115.121.1.15" + }, + { + "id": "schacAttributeType:7", + "NAME": "schacSn2", + "DESC": "Second surname of a person", + "EQUALITY": "caseIgnoreMatch", + "ORDERING": "caseIgnoreOrderingMatch", + "SUBSTR": "caseIgnoreSubstringsMatch", + "SYNTAX": "1.3.6.1.4.1.1466.115.121.1.15" + }, + { + "id": "schacAttributeType:8", + "NAME": "schacPersonalTitle", + "DESC": "RFC1274: personal title", + "EQUALITY": "caseIgnoreMatch", + "SUBSTR": "caseIgnoreSubstringsMatch", + "SINGLE-VALUE": true, + "SYNTAX": "1.3.6.1.4.1.1466.115.121.1.15" + }, + { + "id": "schacAttributeType:9", + "NAME": "schacHomeOrganization", + "DESC": "Domain name of the home organization", + "EQUALITY": "caseIgnoreMatch", + "SUBSTR": "caseIgnoreSubstringsMatch", + "SINGLE-VALUE": true, + "SYNTAX": "1.3.6.1.4.1.1466.115.121.1.15" + }, + { + "id": "schacAttributeType:10", + "NAME": "schacHomeOrganizationType", + "DESC": "Type of the home organization", + "EQUALITY": "caseIgnoreMatch", + "SYNTAX": "1.3.6.1.4.1.1466.115.121.1.15" + }, + { + "id": "schacAttributeType:11", + "NAME": "schacCountryOfResidence", + "DESC": "Country of residence of a person. Format two-letter acronym according to ISO 3166-1 alpha 2", + "EQUALITY": "caseIgnoreMatch", + "SYNTAX": "1.3.6.1.4.1.1466.115.121.1.15" + }, + { + "id": "schacAttributeType:12", + "NAME": "schacUserPresenceID", + "DESC": "Used to store a set of values related to the network presence", + "EQUALITY": "caseExactMatch", + "SUBSTR": "caseExactSubstringsMatch", + "SYNTAX": "1.3.6.1.4.1.1466.115.121.1.15" + }, + { + "id": "schacAttributeType:13", + "NAME": "schacPersonalPosition", + "DESC": "Position inside an institution", + "EQUALITY": "caseIgnoreMatch", + "SUBSTR": "caseIgnoreSubstringsMatch", + "SYNTAX": "1.3.6.1.4.1.1466.115.121.1.15" + }, + { + "id": "schacAttributeType:14", + "NAME": "schacPersonalUniqueCode", + "DESC": "unique code for the subject", + "EQUALITY": "caseIgnoreMatch", + "ORDERING": "caseIgnoreOrderingMatch", + "SUBSTR": "caseIgnoreSubstringsMatch", + "SYNTAX": "1.3.6.1.4.1.1466.115.121.1.15" + }, + { + "id": "schacAttributeType:15", + "NAME": "schacPersonalUniqueID", + "DESC": "Unique identifier for the subject", + "EQUALITY": "caseExactMatch", + "ORDERING": "caseExactOrderingMatch", + "SUBSTR": "caseExactSubstringsMatch", + "SYNTAX": "1.3.6.1.4.1.1466.115.121.1.15" + }, + { + "id": "schacAttributeType:17", + "NAME": "schacExpiryDate", + "DESC": "Date from which the set of data is to be considered invalid (format YYYYMMDDhhmmssZ)", + "EQUALITY": "generalizedTimeMatch", + "ORDERING": "generalizedTimeOrderingMatch", + "SINGLE-VALUE": true, + "SYNTAX": "1.3.6.1.4.1.1466.115.121.1.24" + }, + { + "id": "schacAttributeType:18", + "NAME": "schacUserPrivateAttribute", + "DESC": "Set of denied access attributes", + "EQUALITY": "caseIgnoreIA5Match", + "SUBSTR": "caseIgnoreIA5SubstringsMatch", + "SYNTAX": "1.3.6.1.4.1.1466.115.121.1.26" + }, + { + "id": "schacAttributeType:19", + "NAME": "schacUserStatus", + "DESC": "Used to store a set of status of a person as user of services", + "EQUALITY": "caseIgnoreMatch", + "SUBSTR": "caseIgnoreSubstringsMatch", + "SYNTAX": "1.3.6.1.4.1.1466.115.121.1.15" + }, + { + "id": "schacAttributeType:20", + "NAME": "schacProjectMembership", + "DESC": "Name of the project", + "EQUALITY": "caseIgnoreMatch", + "SUBSTR": "caseIgnoreSubstringsMatch", + "SYNTAX": "1.3.6.1.4.1.1466.115.121.1.15" + }, + { + "id": "schacAttributeType:21", + "NAME": "schacProjectSpecificRole", + "DESC": "Used to store a set of roles of a person inside a project", + "EQUALITY": "caseIgnoreMatch", + "SUBSTR": "caseIgnoreSubstringsMatch", + "SYNTAX": "1.3.6.1.4.1.1466.115.121.1.15" + }, + { + "id": "schacExpAttr:3", + "NAME": "schacYearOfBirth", + "DESC": "Year of birth (format YYYY, only numeric chars)", + "EQUALITY": "numericStringMatch", + "ORDERING": "numericStringOrderingMatch", + "SUBSTR": "numericStringSubstringsMatch", + "SINGLE-VALUE": true, + "SYNTAX": "1.3.6.1.4.1.1466.115.121.1.36" + } + ], + "olcObjectClasses": [ + { + "id": "schacObjectClass:1", + "NAME": "schacPersonalCharacteristics", + "DESC": "Personal characteristics describe the individual person represented by the entry", + "AUXILIARY": true, + "MAY": [ + "schacMotherTongue", + "schacDateOfBirth", + "schacPlaceOfBirth", + "schacCountryOfCitizenship", + "schacSn1", + "schacSn2", + "schacPersonalTitle" + ] + }, + { + "id": "schacObjectClass:2", + "NAME": "schacContactLocation", + "DESC": "Primary means of locating and contacting potential collaborators and other persons-of-interest at peer institutions", + "AUXILIARY": true, + "MAY": [ + "schacHomeOrganization", + "schacHomeOrganizationType", + "schacCountryOfResidence", + "schacUserPresenceID" + ] + }, + { + "id": "schacObjectClass:3", + "NAME": "schacEmployeeInfo", + "DESC": "Employee information includes attributes that have relevance to the employee role, such as position, office hours, and job title", + "AUXILIARY": true, + "MAY": [ + "schacPersonalPosition" + ] + }, + { + "id": "schacObjectClass:4", + "NAME": "schacLinkageIdentifiers", + "DESC": "Used to link a directory entry with records in external data stores or other directory entries", + "AUXILIARY": true, + "MAY": [ + "schacPersonalUniqueCode", + "schacPersonalUniqueID" + ] + }, + { + "id": "schacObjectClass:5", + "NAME": "schacEntryMetadata", + "DESC": "Used to contain information about the entry itself, often its status, birth, and death", + "AUXILIARY": true, + "MAY": [ + "schacExpiryDate" + ] + }, + { + "id": "schacObjectClass:6", + "NAME": "schacEntryConfidentiality", + "DESC": "Used to indicate whether an entry is visible publicly, visible only to affiliates of the institution, or not visible at all", + "AUXILIARY": true, + "MAY": [ + "schacUserPrivateAttribute" + ] + }, + { + "id": "schacObjectClass:7", + "NAME": "schacUserEntitlements", + "DESC": "Authorization for services", + "AUXILIARY": true, + "MAY": [ + "schacUserStatus" + ] + }, + { + "id": "schacObjectClass:8", + "NAME": "schacGroupMembership", + "DESC": "Groups used to provide/restrict authorization to entries and attributes", + "AUXILIARY": true, + "MAY": [ + "schacProjectMembership", + "schacProjectSpecificRole" + ] + }, + { + "id": "schacExpObjClass:1", + "NAME": "schacExperimentalOC", + "DESC": "Experimental Object Class", + "AUXILIARY": true, + "MAY": [ + "schacYearOfBirth" + ] + } + ] +} \ No newline at end of file From c5ab7ec60c668a0439b1f9cab6e6c46445ae5b87 Mon Sep 17 00:00:00 2001 From: Niels van Dijk Date: Mon, 15 Sep 2025 15:29:59 +0200 Subject: [PATCH 3/3] Added support for VCDM and SD-JWT --- schema/vc/mkSchema.py | 43 ++++++---- schema/vc/schac_1_6_0.json_sdjwt.json | 82 +++++++++++++++++++ ..._1_6_0.json => schac_1_6_0.json_vcdm.json} | 2 +- 3 files changed, 109 insertions(+), 18 deletions(-) create mode 100644 schema/vc/schac_1_6_0.json_sdjwt.json rename schema/vc/{schac_1_6_0.json => schac_1_6_0.json_vcdm.json} (98%) diff --git a/schema/vc/mkSchema.py b/schema/vc/mkSchema.py index 3767807..a524ec2 100755 --- a/schema/vc/mkSchema.py +++ b/schema/vc/mkSchema.py @@ -81,21 +81,9 @@ def mapType(equality, multivalued = False): return None def main(argv): - schemaFile = "schac_1_6_0.json" - - schema = { - "$id": "https://refeds.org/schemas/vc/schac_1_6_0.json", - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$comment": "This schema implements SCHAC (SCHema for ACademia) version 1.6.0 (23 May 2022) - https://refeds.org/specifications/schac - This schema is maintained by REFEDs (https://refeds.org). The mission of REFEDS (the Research and Education FEDerations group) is to be the voice that articulates the mutual needs of research and education identity federations worldwide.", - "title": "SCHACcredentials", - "description": "Schema for verifiable credential claims describing a user in the context of SCHAC", - "type": "object", - "properties": { - "credentialSubject": { - "type": "object", - "properties": {} - } - } + schemaFile = { + "vcdm2.0": "schac_1_6_0.json_vcdm.json", + "sdjwt": "schac_1_6_0.json_sdjwt.json" } # Generate object properties based on ldif schema,coverted to json by chatGTP @@ -129,8 +117,29 @@ def main(argv): case "schacCountryOfCitizenship" | "schacCountryOfResidence": object_props[name]['maxLength'] = 2 - schema["properties"]["credentialSubject"]["properties"] = object_props - write_file(schema, schemaFile, mkpath=False, overwrite=True, type='json') + for type,filename in schemaFile.items(): + schema = { + "$id": "https://refeds.org/schemas/vc/" + filename, + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$comment": "This schema implements SCHAC (SCHema for ACademia) version 1.6.0 (23 May 2022) - https://refeds.org/specifications/schac - This schema is maintained by REFEDs (https://refeds.org). The mission of REFEDS (the Research and Education FEDerations group) is to be the voice that articulates the mutual needs of research and education identity federations worldwide.", + "title": "SCHACcredentials", + "description": "Schema for verifiable credential claims describing a user in the context of SCHAC", + "type": "object", + "properties": { + "credentialSubject": { + "type": "object", + "properties": {} + } + } + } + + match type: + case "vcdm2.0": + schema["properties"]["credentialSubject"]["properties"] = object_props + case "sdjwt": + schema["properties"] = object_props + + write_file(schema, filename, mkpath=False, overwrite=True, type='json') if __name__ == "__main__": main(sys.argv[1:]) \ No newline at end of file diff --git a/schema/vc/schac_1_6_0.json_sdjwt.json b/schema/vc/schac_1_6_0.json_sdjwt.json new file mode 100644 index 0000000..6ecb65e --- /dev/null +++ b/schema/vc/schac_1_6_0.json_sdjwt.json @@ -0,0 +1,82 @@ +{ + "$id":"https://refeds.org/schemas/vc/schac_1_6_0.json_sdjwt.json", + "$schema":"https://json-schema.org/draft/2020-12/schema", + "$comment":"This schema implements SCHAC (SCHema for ACademia) version 1.6.0 (23 May 2022) - https://refeds.org/specifications/schac - This schema is maintained by REFEDs (https://refeds.org). The mission of REFEDS (the Research and Education FEDerations group) is to be the voice that articulates the mutual needs of research and education identity federations worldwide.", + "title":"SCHACcredentials", + "description":"Schema for verifiable credential claims describing a user in the context of SCHAC", + "type":"object", + "properties":{ + "schac_mother_tongue":{ + "type":"string", + "description":"RFC 3066 code for preferred language of communication" + }, + "schac_gender":{ + "type":"integer", + "description":"Representation of human sex (see ISO 5218). Deprecated as of SCHAC 1.6.0", + "deprecated":true + }, + "schac_date_of_birth":{ + "type":"number", + "description":"Date of birth (format YYYYMMDD, only numeric chars)", + "maxLength":8 + }, + "schac_place_of_birth":{ + "type":"string", + "description":"Birth place of a person" + }, + "schac_country_of_citizenship":{ + "type":"string", + "description":"Country of citizenship of a person. Format two-letter acronym according to ISO 3166-1 alpha 2", + "maxLength":2 + }, + "schac_sn1":{ + "type":"string", + "description":"First surname of a person" + }, + "schac_sn2":{ + "type":"string", + "description":"Second surname of a person" + }, + "schac_personal_title":{ + "type":"string", + "description":"RFC1274: personal title" + }, + "schac_home_organization":{ + "type":"string", + "description":"Domain name of the home organization" + }, + "schac_home_organization_type":{ + "type":"array", + "description":"Type of the home organization", + "anyof":{ + "type":"uri" + } + }, + "schac_country_of_residence":{ + "type":"string", + "description":"Country of residence of a person. Format two-letter acronym according to ISO 3166-1 alpha 2", + "maxLength":2 + }, + "schac_user_presence_id":{ + "type":"array", + "description":"Used to store a set of values related to the network presence", + "anyof":{ + "type":"uri" + } + }, + "schac_personal_position":{ + "type":"array", + "description":"Position inside an institution", + "anyof":{ + "type":"uri" + } + }, + "schac_personal_unique_code":{ + "type":"array", + "description":"unique code for the subject", + "anyof":{ + "type":"uri" + } + } + } +} \ No newline at end of file diff --git a/schema/vc/schac_1_6_0.json b/schema/vc/schac_1_6_0.json_vcdm.json similarity index 98% rename from schema/vc/schac_1_6_0.json rename to schema/vc/schac_1_6_0.json_vcdm.json index f1b39b9..0973dba 100644 --- a/schema/vc/schac_1_6_0.json +++ b/schema/vc/schac_1_6_0.json_vcdm.json @@ -1,5 +1,5 @@ { - "$id":"https://refeds.org/schemas/vc/schac_1_6_0.json", + "$id":"https://refeds.org/schemas/vc/schac_1_6_0.json_vcdm.json", "$schema":"https://json-schema.org/draft/2020-12/schema", "$comment":"This schema implements SCHAC (SCHema for ACademia) version 1.6.0 (23 May 2022) - https://refeds.org/specifications/schac - This schema is maintained by REFEDs (https://refeds.org). The mission of REFEDS (the Research and Education FEDerations group) is to be the voice that articulates the mutual needs of research and education identity federations worldwide.", "title":"SCHACcredentials",