Appendix C - Cryptography and Encoding

This appendix defines the cryptographic and encoding boundaries used by OpenAgenet (OAN) for identity, credentials, resource publication, and cross-node verification. The current shared Rust implementation is primarily based on oan-crypto, oan-credentials, and oan-did-oan; the browser-side TypeScript SDK uses the Web Cryptography API to generate Ed25519 Local Identity. Any cross-language implementation must recompute the same digest from the same canonicalized input and must handle algorithms, encodings, and error states explicitly.

C.1 Supported Algorithms

The current CryptoSuite includes Ed25519Sha256Legacy, Ed25519Sha256, and Sm2Sm3: the first two use Ed25519 signatures and SHA-256 hashing, while the latter uses SM2 signatures and SM3 hashing. The corresponding verification method types are Ed25519VerificationKey2020 or SM2VerificationKey2020, and the proof types are Ed25519Signature2020 or SM2Signature2020. The algorithm suite must be consistent with the public key, proof, and hash algorithm; unknown suites are rejected, and legacy suites may only be retained under a compatibility policy and must not be silently downgraded.

Algorithm Suite Signature Algorithm Hash Algorithm Typical Use
Ed25519Sha256Legacy Ed25519 SHA-256 Compatibility with existing credential or digest flows
Ed25519Sha256 Ed25519 SHA-256 Current Ed25519 proof flow
Sm2Sm3 SM2 SM3 Identity and proof flows supporting SM2

The algorithm suite, verification method, public key encoding, and proof type must match as a set; unknown or deprecated suites must not silently switch to another algorithm suite.

In implementation terms, Ed25519Sha256Legacy and Ed25519Sha256 use the same Ed25519 key and SHA-256 hash, but their signing inputs differ: the former first computes SHA-256 over the canonicalized JSON, then signs the UTF-8 bytes of the resulting 64 lowercase hexadecimal characters; the latter directly signs the UTF-8 bytes of the canonicalized JSON. Sm2Sm3 directly uses SM2 to sign the UTF-8 bytes of the canonicalized JSON, and its digest function uses SM3. A cross-language implementation must not determine the input solely from the signature algorithm name; it must read both cryptoSuite and hashAlgorithm.

Selection Decision in the Current Implementation Failure Handling
cryptoSuite Determines the signature algorithm, hash algorithm, and signature input form Reject when unrecognized
proof.type The Ed25519Signature2020 or SM2Signature2020 corresponding to the suite Reject when the type does not match
Verification method type Derives the suite from Ed25519VerificationKey2020 or SM2VerificationKey2020 Reject when it cannot be derived
Public key carrier Use either publicKeyMultibase or publicKeyJwk Reject when both are missing or cannot be parsed

C.2 Canonicalized JSON and Hashing

The shared implementation first converts the serializable object to JSON, then recursively orders object keys lexicographically; arrays retain their original order, strings use JSON escaping, numbers use their serialized representation, and the rule for omitting empty fields is determined by the Rust/TypeScript data model. Ed25519Sha256Legacy signs the SHA-256 hexadecimal result of the canonicalized JSON, while Ed25519Sha256 and Sm2Sm3 sign the UTF-8 bytes of the canonicalized JSON. Cross-language implementations must fix the rules for UTF-8, key ordering, numbers, and empty fields; any difference will cause digest or signature verification to fail.

{
  "b": 2,
  "a": {
    "y": 1,
    "x": "OAN"
  },
  "items": ["one", "two"]
}

During canonicalization, keys are recursively ordered lexicographically, arrays retain their original order, and the input uses UTF-8. The corresponding canonicalized result is:

{
  "a": {
    "x": "OAN",
    "y": 1
  },
  "b": 2,
  "items": ["one", "two"]
}

Cross-language tests should fix the rules for null values, number representations, string escaping, and omission of object fields at the same time, rather than relying solely on the default serialization behavior of a particular language runtime.

For example, the SHA-256 result of the canonicalized string above is 03332fb018e2f0376e263dabf4b2231e3bd70767e19fb7e263060efb68476e02. This value corresponds only to the example object, example key order, and example UTF-8 encoding; it is not a protocol constant. On the Rust side, oan-crypto::canonical_json and hash_json_with_suite can be used to produce a baseline; on the TypeScript side, the object should first be recursively sorted according to the same rules, and crypto.subtle.digest("SHA-256", ...) should then be used to compute the digest over the UTF-8 bytes.

Implementation must distinguish three changes in particular: changing object member order should not change the result after canonicalization, changing array element order will change the result, and whether changing a number from 1 to 1.0 is considered equivalent depends on the JSON number representation before the input enters the canonicalization function. Production code must not first convert JSON to indented text and then sign it directly, nor may it rely on the order of fields returned by a database.

C.3 JWS and Detached Signatures

The current OAN shared implementation uses DataIntegrityProof to express detached proofs, containing type, creator, created, proofPurpose, proofValue, cryptoSuite, hashAlgorithm, and verificationMethod. proofValue is an unpadded Base64url-encoded signature value; the signature input is determined by the canonicalized result of the object to be signed and the algorithm suite. The verifier must first determine the suite, reconstruct the signature input, and verify the public key and signature, then check the creator, verification method, target DID, validity period, and status; it must not merely compare proofValue strings.

{
  "type": "DataIntegrityProof",
  "creator": "did:oan:SKDM:ExampleResourceDid#key-1",
  "created": "2026-09-07T01:02:03Z",
  "proofPurpose": "assertionMethod",
  "verificationMethod": "did:oan:SKDM:ExampleResourceDid#key-1",
  "cryptoSuite": "Ed25519Sha256",
  "hashAlgorithm": "SHA-256",
  "proofValue": "base64url-without-padding"
}

Signature verification failures should distinguish inconsistent canonicalized input, a mismatched public key, an invalid signature, an incorrect proof purpose, and an unacceptable status, so that the caller can decide whether to correct the request, retrieve the data again, or stop processing.

The minimum signature verification sequence is as follows:

  1. Determine the suite from proof.cryptoSuite or a compatible proof.type, and confirm that it is consistent with the verification method and public key format.
  2. Locate the public key in the DID Document by verificationMethod, and check the relationship among the method identifier, controller, and target DID.
  3. Remove proof from the object being verified and reconstruct the canonicalized signature input according to the suite; legacy suites must also first compute the SHA-256 hexadecimal digest.
  4. Decode proofValue as unpadded Base64url and perform signature verification, then check created, purpose, issuer authorization, and credential status.
payload without proof -> canonical JSON -> suite-specific signature input
                                      -> Base64url proofValue -> verify with DID public key

The fact that proofValue can be decoded only shows that its encoding is readable; it does not show that the signature is correct. A correct signature also does not automatically show that the issuer has authorization from the Root platform or that the resource remains available.

C.4 DID and Key Encoding

The current syntax for a resource DID is did:oan:<four uppercase semantic code characters>:<32-character Base58 suffix>; the suffix uses the Bitcoin Base58 character set and excludes 0, O, I, and l. The did:oan parser also validates the correspondence between the resource type and the semantic-code subject portion. Depending on the algorithm, public keys may use a z-prefixed Base58 multibase or a JWK: Ed25519 uses kty=OKP, crv=Ed25519, and an unpadded Base64url x; SM2 uses kty=EC, crv=SM2, and x and y. Private keys must not enter a DID Document, ordinary protocol request, or service log.

did:oan:<four uppercase semantic code characters>:<32-character Base58 suffix>
Material Recommended Representation Must Not Appear
Resource DID did:oan string Incorrect semantic code, illegal Base58 character
Ed25519 public key multibase or Ed25519 JWK Private key, raw string with no declared algorithm
SM2 public key SM2 JWK or a public key representation supported by the implementation Incomplete x/y coordinates
Private key Client-protected storage DID Document, ordinary request, service log

publicKeyMultibase currently consists of a z prefix followed by the Base58 encoding of the public key bytes; Ed25519 uses a 32-byte public key, while SM2 uses uncompressed SEC1 public key bytes. JWK uses unpadded Base64url-encoded fields; after decoding, Ed25519 x should be 32 bytes, and SM2 x and y should each be 32 bytes. oan-crypto parses these carriers according to the verification method or explicit cryptoSuite, and performs signature verification only after parsing succeeds.

The semantic code of an example DID expresses only the subject or resource type and the application domain; the public key is not carried directly by the suffix. A randomly generated DID and its key pair are generated in two independent steps. If repeatable derivation is required by the business logic, DidOan::derive control material and a nonce must be used explicitly, and the derivation rule must be treated as an implementation-specific capability; a randomly generated DID must not be treated as an identifier that can be reverse-derived from a public key.

C.5 VC and VP Encoding

Registration VCs and node authorization VCs use JSON objects to express the issuer, subject, status, issuance/expiration times, claims, and proof; the shared credential implementation is based on AgentRegistrationCredential, NodeAuthorizationCredential, and DataIntegrityProof. At issuance, a signature is generated over the object without proof; at verification, proof is removed to reconstruct the original payload, which is then verified according to the suite specified by the proof. If a VP is presented by an external system, its original structure, credential references, and verification results should be retained; parseability does not mean acceptance under the OAN trust policy, and expired, revoked, subject-mismatched, or unauthorized issuers must be rejected or marked unacceptable.

{
  "type": ["VerifiableCredential", "AgentRegistrationCredential"],
  "issuer": "did:oan:REGS:RegistrarExample",
  "credentialSubject": {
    "id": "did:oan:SKDM:ExampleResourceDid"
  },
  "proof": { "type": "DataIntegrityProof", "proofValue": "..." }
}

Successful VC parsing only means that the structure is readable; the caller must still verify the signature, issuer authorization, subject, validity period, and credential status. When a VP serves as an external presentation container, the credential source and verification result should be retained; the act of presentation itself must not be treated as authorization.

The verification object for a registration VC is the AgentRegistrationCredential with proof removed; the verification object for a node authorization VC is the NodeAuthorizationCredential with proof removed. The following table can be used as the implementation order for the verification chain:

Check Object of Attention Meaning When Not Passed
Structure type, issuer, subject, time, claims, proof The credential cannot be parsed or proceed to subsequent verification
Signature proof, public key in the issuer’s DID Document The credential content is not proven by the corresponding private key
Binding issuer, subject, resource DID, DID Document hash, and ResourcePackage hash The credential does not correspond to the current resource
Authorization Issuer identity, node role, authorization domain, and governance status The issuer cannot make this statement within the current boundary
Status status, issuedAt, expiresAt, and available revocation facts The credential is invalid or should no longer be used

The verifier should retain enough of the original credential, DID Document, verification time, and error code to support re-verification; it should not store only a boolean value of “verification succeeded”.

C.6 Key Backup and Recovery Format

Local Identity backups are sensitive client files and may contain the version, update time, default subject/agent identifiers, subject/agent/node records, DID Documents, public and private key JWKs, and resource profiles. The TypeScript SDK currently uses an identity storage snapshot with version 1; identity backup files downloaded through the website registration flow include a time identifier in the filename, and after a successful resource submission, the registration VC, DID Document, and Local Identity file may also be packaged for storage. Backup files should be kept privately by the user. On import, the JSON structure, version, public/private key correspondence, and DID consistency must be checked; unsupported versions, missing keys, or parse failures must not result in a partial load.

{
  "version": 1,
  "updatedAt": "2026-09-07T01:02:03Z",
  "defaultSubjectDid": "did:oan:SUBJ:ExampleSubjectDid",
  "subjects": [],
  "agents": [],
  "importedNodes": []
}

The import flow should complete the integrity and version checks first, then load subject, agent, and node records as one operation. If a public and private key do not match, the DID is inconsistent, or the version is unsupported, the entire backup should be rejected; a partially usable identity must not be generated.

The private key JWK in a backup file is highly sensitive material. Browser download, import, and re-export should all be actively initiated by the user; the page, Registrar node, Root platform, and Discovery node do not need to receive this private key. After import, the client may sign a test message with the private key and verify it with the public key from the backup to confirm that the key pair matches; the private key or test signature must not be written to logs. The time identifier in the backup filename is used only to distinguish downloaded files and does not participate in identity or signature computation.

{
  "version": 1,
  "subjects": [
    {
      "did": "did:oan:DVFI:7YpQm9Kx2VnRb6Ts3WfHa4Cd5Ej8LgNz",
      "verificationMethodId": "did:oan:DVFI:7YpQm9Kx2VnRb6Ts3WfHa4Cd5Ej8LgNz#key-1",
      "publicKeyJwk": { "kty": "OKP", "crv": "Ed25519", "x": "example-public-x" },
      "privateKeyJwk": { "kty": "OKP", "crv": "Ed25519", "x": "example-public-x", "d": "example-private-d" }
    }
  ],
  "agents": [],
  "nodes": []
}

The key values in the example above are unusable placeholders intended only to show field positions; a real backup must not use this example content, and production private keys must not be placed in public documentation or test output.

C.7 DID and Resource Identifier Test Vectors

Test vectors should cover valid DIDs for the four resource types, infrastructure node DIDs, organization and developer DIDs, as well as semantic-code case errors, partial omissions, incorrect separators, incorrect suffix lengths, prohibited Base58 characters, extra segments, and resource-type mismatches. Each vector fixes the input string and the parse result or error type; generated DIDs validate only the format and type relationship, and a random suffix must not be required to be identical across runs. Cross-language tests should use the same JSON encoding and algorithm suite, and should store the expected digest or signature verification result.

Vector Category Input Passing Criterion
Valid DID Four resource types and infrastructure DIDs Parsing succeeds and the type relationship is correct
Format error Incorrect case, separator, or length A parse error is returned consistently
Prohibited character 0, O, I, l, etc. Reject without silently correcting
Type mismatch Semantic code does not match the resource type Reject or return an explicit error

Test vectors should also record the input, expected error, and side-effect criteria. For example, did:oan:SKFI:7YpQm9Kx2VnRb6Ts3WfHa4Cd5Ej8LgNz should pass syntax parsing and the skill type check; submitting the same DID as an mcp_server resource should produce a type mismatch error. Replacing a suffix character with 0, O, I, or l should fail during parsing, and the client must not rewrite it automatically before submission.

Test Field Example Value Expected Result
Valid semantic code SKFI Parsed as the SK subject code and FI application-domain code
Valid suffix 7YpQm9Kx2VnRb6Ts3WfHa4Cd5Ej8LgNz Length is 32 and contains only permitted Base58 characters
Resource type match SKFI + skill Type check passes
Resource type conflict SKFI + mcp_server Returns ResourceTypeMismatch or the corresponding error code

C.8 Hash and Signature Verification Fixtures

Fixtures should include at least the original canonicalization input, canonicalized JSON, algorithm suite, hash algorithm, digest, signature input, public key, proof, and expected verification result, and should separately cover the correct payload, field-order changes, number/string changes, public key changes, truncated signatures, and proof-suite mismatches. The Rust implementation may use oan-crypto tests to produce the baseline, and the TypeScript implementation should read the same fixture to recompute it. Fixtures must not contain production private keys; dedicated test keys should be used when generating fixtures, with the public keys and signature results made available.

A fixture is recommended to use one JSON file that can be read by both Rust and TypeScript, with the input object and expected result stored separately. An example structure is:

{
  "suite": "Ed25519Sha256",
  "payload": { "a": 1, "b": "OAN" },
  "canonicalJson": "{\"a\":1,\"b\":\"OAN\"}",
  "signatureInputEncoding": "utf-8-canonical-json",
  "publicKeyJwk": { "kty": "OKP", "crv": "Ed25519", "x": "test-public-key" },
  "proofValue": "test-signature-base64url-no-padding",
  "expected": { "canonicalJson": true, "signatureValid": true }
}

The public key, signature, and digest in a fixture must be generated from the same dedicated test key set; the x and proofValue in the example remain structural placeholders and cannot be used directly as inputs for a passing test. At least the following negative variants should be run: change only JSON member order, change array order, change one string, replace the public key, truncate the signature, and cross-replace cryptoSuite and proof.type. A correct implementation should accept only the original payload paired with the matching public key, and should produce stable failures for the other variants.

References

Source Type Link
oan-protocol-common Code repository: cryptographic suites, keys, signatures, and hash implementations https://github.com/wolfbrother/oan-protocol-common
oan-sdk-ts Code repository: client cryptographic materials and DID operations https://github.com/OpenAgenet/oan-sdk-ts
did:oan DID Method Specification Standard/method specification: DID identifiers and verification methods https://github.com/OpenAgenet/oan-public-docs/blob/main/did-oan-specs/doc/OAN DID Method Specification.md
OAN Yellow Paper arXiv yellow paper: cryptographic proofs and trust model https://arxiv.org/abs/2606.03163
On this page