Skip to content

feat(castor): now castor and agents can create a did with any keys #199

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

Merged
merged 1 commit into from
Mar 5, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
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
30 changes: 27 additions & 3 deletions EdgeAgentSDK/Castor/Sources/CastorImpl+Public.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,28 @@ extension CastorImpl: Castor {
// ).compute()
// }

public func createDID(
method: DIDMethod,
keys: [(KeyPurpose, any PublicKey)],
services: [DIDDocument.Service]
) throws -> DID {
switch method {
case "prism":
return try CreatePrismDIDOperation(
apollo: apollo,
keys: keys,
services: services
).compute()
case "peer":
return try CreatePeerDIDOperation(
keys: keys,
services: services
).compute()
default:
throw CastorError.noResolversAvailableForDIDMethod(method: method)
}
}

/// createPrismDID creates a DID for a prism (a device or server that acts as a DID owner and controller) using a given master public key and list of services. This function may throw an error if the master public key or services are invalid.
///
/// - Parameters:
Expand All @@ -42,7 +64,7 @@ extension CastorImpl: Castor {
) throws -> DID {
try CreatePrismDIDOperation(
apollo: apollo,
masterPublicKey: masterPublicKey,
keys: [(KeyPurpose.master, masterPublicKey)],
services: services
).compute()
}
Expand All @@ -61,8 +83,10 @@ extension CastorImpl: Castor {
services: [DIDDocument.Service]
) throws -> DID {
try CreatePeerDIDOperation(
autenticationPublicKey: authenticationPublicKey,
agreementPublicKey: keyAgreementPublicKey,
keys: [
(KeyPurpose.authentication, authenticationPublicKey),
(KeyPurpose.agreement, keyAgreementPublicKey)
],
services: services
).compute()
}
Expand Down
42 changes: 27 additions & 15 deletions EdgeAgentSDK/Castor/Sources/DID/PrismDID/PrismDIDPublicKey.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,15 +44,15 @@ struct PrismDIDPublicKey {
case .issuingKey:
return "issuing\(index)"
case .capabilityDelegationKey:
return "capabilityDelegationKey\(index)"
return "capability-delegationKey\(index)"
case .capabilityInvocationKey:
return "capabilityInvocationKey\(index)"
return "capability-invocationKey\(index)"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why have these changed? is this something that needs to be mirrored in other SDKs?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Short answer is not really defined here: https://github.com/input-output-hk/prism-did-method-spec/blob/main/w3c-spec/PRISM-method.md

Long answer: It should be defined plus it seems the examples suggest it should be like that and on the cloud agent its like that, so lets go for it as the default.

case .authenticationKey:
return "authentication\(index)"
case .revocationKey:
return "revocation\(index)"
case .keyAgreementKey:
return "keyAgreement\(index)"
return "key-agreement\(index)"
case .unknownKey:
return "unknown\(index)"
}
Expand Down Expand Up @@ -102,22 +102,34 @@ struct PrismDIDPublicKey {
var protoKey = Io_Iohk_Atala_Prism_Protos_PublicKey()
protoKey.id = id
protoKey.usage = usage.toProto()
guard
let pointXStr = keyData.getProperty(.curvePointX),
let pointYStr = keyData.getProperty(.curvePointY),
let pointX = Data(base64URLEncoded: pointXStr),
let pointY = Data(base64URLEncoded: pointYStr)
else {
switch curve {
case "Ed25519", "X25519":
var protoEC = Io_Iohk_Atala_Prism_Protos_CompressedECKeyData()
protoEC.data = keyData.raw
protoEC.curve = curve
protoKey.keyData = .compressedEcKeyData(protoEC)
case "secp256k1":
guard
let pointXStr = keyData.getProperty(.curvePointX),
let pointYStr = keyData.getProperty(.curvePointY),
let pointX = Data(base64URLEncoded: pointXStr),
let pointY = Data(base64URLEncoded: pointYStr)
else {
throw ApolloError.missingKeyParameters(missing: [
KeyProperties.curvePointX.rawValue,
KeyProperties.curvePointY.rawValue
])
}
var protoEC = Io_Iohk_Atala_Prism_Protos_ECKeyData()
protoEC.x = pointX
protoEC.y = pointY
protoEC.curve = curve
protoKey.keyData = .ecKeyData(protoEC)
default:
throw ApolloError.missingKeyParameters(missing: [
KeyProperties.curvePointX.rawValue,
KeyProperties.curvePointY.rawValue
])
}
var protoEC = Io_Iohk_Atala_Prism_Protos_ECKeyData()
protoEC.x = pointX
protoEC.y = pointY
protoEC.curve = curve
protoKey.keyData = .ecKeyData(protoEC)
return protoKey
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,21 @@ import PeerDID

struct CreatePeerDIDOperation {
private let method: DIDMethod = "peer"
let autenticationPublicKey: PublicKey
let agreementPublicKey: PublicKey
let keys: [(KeyPurpose, PublicKey)]
let services: [Domain.DIDDocument.Service]

func compute() throws -> Domain.DID {
let authenticationKeys = try keys
.filter { $0.0 == .authentication }
.map(\.1)
.map(authenticationFromPublicKey(publicKey:))
let agreementKeys = try keys
.filter { $0.0 == .agreement }
.map(\.1)
.map(keyAgreementFromPublicKey(publicKey:))
let did = try PeerDIDHelper.createAlgo2(
authenticationKeys: [authenticationFromPublicKey(publicKey: autenticationPublicKey)],
agreementKeys: [keyAgreementFromPublicKey(publicKey: agreementPublicKey)],
authenticationKeys: authenticationKeys,
agreementKeys: agreementKeys,
services: services.flatMap { service in
service.serviceEndpoint.map {
AnyCodable(dictionaryLiteral:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,29 +5,33 @@ import Foundation
struct CreatePrismDIDOperation {
private let method: DIDMethod = "prism"
let apollo: Apollo
let masterPublicKey: PublicKey
let keys: [(KeyPurpose, PublicKey)]
let services: [DIDDocument.Service]

func compute() throws -> DID {
var operation = Io_Iohk_Atala_Prism_Protos_AtalaOperation()
guard let masterKeyCurve = masterPublicKey.getProperty(.curve) else {
throw CastorError.invalidPublicKeyCoding(didMethod: "prism", curve: "no curve")
guard keys.count(where: { $0.0 == .master} ) == 1 else {
throw CastorError.requiresOneAndJustOneMasterKey
}
let groupByPurpose = Dictionary(grouping: keys, by: { $0.0 })
operation.createDid = try createDIDAtalaOperation(
publicKeys: [PrismDIDPublicKey(
apollo: apollo,
id: PrismDIDPublicKey.Usage.authenticationKey.defaultId,
curve: masterKeyCurve,
usage: .authenticationKey,
keyData: masterPublicKey
),
PrismDIDPublicKey(
apollo: apollo,
id: PrismDIDPublicKey.Usage.masterKey.defaultId,
curve: masterKeyCurve,
usage: .masterKey,
keyData: masterPublicKey
)],
publicKeys: groupByPurpose.flatMap { (key, value) in
try value
.sorted(by: { $0.1.identifier < $1.1.identifier } )
.enumerated()
.map {
guard let curve = $0.element.1.getProperty(.curve) else {
throw CastorError.invalidPublicKeyCoding(didMethod: "prism", curve: "no curve")
}
return PrismDIDPublicKey(
apollo: apollo,
id: key.toPrismDIDKeyPurpose().id(index: $0.offset),
curve: curve,
usage: key.toPrismDIDKeyPurpose(),
keyData: $0.element.1
)
}
},
services: services
)
return try createLongFormFromOperation(method: method, atalaOperation: operation)
Expand Down Expand Up @@ -68,3 +72,24 @@ struct CreatePrismDIDOperation {
return DID(method: method, methodId: methodSpecificId.description)
}
}

extension KeyPurpose {
func toPrismDIDKeyPurpose() -> PrismDIDPublicKey.Usage {
switch self {
case .master:
return .masterKey
case .issue:
return .issuingKey
case .authentication:
return .authenticationKey
case .capabilityDelegation:
return .capabilityDelegationKey
case .capabilityInvocation:
return .capabilityInvocationKey
case .agreement:
return .keyAgreementKey
case .revocation:
return .revocationKey
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -112,25 +112,34 @@ struct LongFormPrismDIDResolver: DIDResolverDomain {
serviceEndpoint: $0.serviceEndpoint.map { .init(uri: $0) }
)
}

let decodedPublicKeys = publicKeys.enumerated().map {
let didUrl = DIDUrl(
did: did,
fragment: $0.element.usage.id(index: $0.offset - 1)
)

let method = DIDDocument.VerificationMethod(
id: didUrl,
controller: did,
type: $0.element.keyData.getProperty(.curve) ?? "",
publicKeyMultibase: $0.element.keyData.raw.base64EncodedString()
)

return PublicKeyDecoded(
id: didUrl.string,
keyType: .init(usage: $0.element.usage),
method: method
)
let groupByPurpose = Dictionary(
// Per specification master keys and revocation keys should not be in the did document
grouping: publicKeys.filter { $0.usage != .masterKey && $0.usage != .revocationKey },
by: { $0.usage }
)
let decodedPublicKeys = groupByPurpose.flatMap { (key, value) in
value
.sorted(by: { $0.id < $1.id } )
.enumerated()
.map {
let didUrl = DIDUrl(
did: did,
fragment: $0.element.usage.id(index: $0.offset)
)

let method = DIDDocument.VerificationMethod(
id: didUrl,
controller: did,
type: $0.element.keyData.getProperty(.curve) ?? "",
publicKeyMultibase: $0.element.keyData.raw.base64EncodedString()
)

return PublicKeyDecoded(
id: didUrl.string,
keyType: .init(usage: $0.element.usage),
method: method
)
}
}

return (decodedPublicKeys, services)
Expand Down
22 changes: 22 additions & 0 deletions EdgeAgentSDK/Domain/Sources/BBs/Castor.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
import Foundation

public enum KeyPurpose: String, Hashable, Equatable, CaseIterable {
case master
case issue
case capabilityDelegation
case capabilityInvocation
case authentication
case revocation
case agreement
}

/// The Castor protocol defines the set of decentralized identifier (DID) operations that are used in the Atala PRISM architecture. It provides a way for users to create, manage, and control their DIDs and associated cryptographic keys.
public protocol Castor {
/// parseDID parses a string representation of a Decentralized Identifier (DID) into a DID object. This function may throw an error if the string is not a valid DID.
Expand All @@ -8,6 +18,18 @@ public protocol Castor {
/// - Returns: The DID object
/// - Throws: An error if the string is not a valid DID
func parseDID(str: String) throws -> DID

/// createDID creates a DID for a method using a given an array of public keys and list of services. This function may throw an error.
/// - Parameters:
/// - method: DID Method to use (ex: prism, peer)
/// - keys: An array of Tuples with the public key and the key purpose
/// - services: The list of services
/// - Returns: The created DID
func createDID(
method: DIDMethod,
keys: [(KeyPurpose, PublicKey)],
services: [DIDDocument.Service]
) throws -> DID

/// createPrismDID creates a DID for a prism (a device or server that acts as a DID owner and controller) using a given master public key and list of services. This function may throw an error if the master public key or services are invalid.
///
Expand Down
1 change: 1 addition & 0 deletions EdgeAgentSDK/Domain/Sources/BBs/Pollux.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ public enum CredentialOperationsOptions {
case entropy(String) // Entropy for any randomization operation.
case signableKey(SignableKey) // A key that can be used for signing.
case exportableKey(ExportableKey) // A key that can be exported.
case exportableKeys([ExportableKey]) // A key that can be exported.
case zkpPresentationParams(attributes: [String: Bool], predicates: [String]) // Anoncreds zero-knowledge proof presentation parameters
case disclosingClaims(claims: [String])
case thid(String)
Expand Down
7 changes: 7 additions & 0 deletions EdgeAgentSDK/Domain/Sources/Models/Errors.swift
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,9 @@ public enum CastorError: KnownPrismError {
/// An error case representing inability to retrieve the public key from a document.
case cannotRetrievePublicKeyFromDocument

/// An error case representing that a master key was not provided or that it had more than one
case requiresOneAndJustOneMasterKey

/// The error code returned by the server.
public var code: Int {
switch self {
Expand All @@ -400,6 +403,8 @@ public enum CastorError: KnownPrismError {
return 29
case .cannotRetrievePublicKeyFromDocument:
return 30
case .requiresOneAndJustOneMasterKey:
return 31
}
}

Expand Down Expand Up @@ -432,6 +437,8 @@ public enum CastorError: KnownPrismError {
return "No resolvers in castor are able to resolve the method \(method), please provide a resolver"
case .cannotRetrievePublicKeyFromDocument:
return "The public keys in the DIDDocument are not in multibase or the multibase is invalid"
case .requiresOneAndJustOneMasterKey:
return "The array contains none or more than one master key"
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -180,12 +180,20 @@ public extension DIDCommAgent {
.first()
.await()

guard let storedPrivateKey = didInfo?.privateKeys.first else { throw EdgeAgentError.cannotFindDIDKeyPairIndex }
let downloader = DownloadDataWithResolver(castor: castor)
guard
let attachment = offer.attachments.first,
let offerFormat = attachment.format
else {
throw PolluxError.unsupportedIssuedMessage
}

guard let storedPrivateKey = didInfo?.privateKeys else { throw EdgeAgentError.cannotFindDIDKeyPairIndex }

let privateKey = try await apollo.restorePrivateKey(storedPrivateKey)
let privateKeys = try await storedPrivateKey.asyncMap { try await apollo.restorePrivateKey($0) }
let exporting = privateKeys.compactMap(\.exporting)

guard
let exporting = privateKey.exporting,
let linkSecret = try await pluto.getLinkSecret().first().await()
else { throw EdgeAgentError.cannotFindDIDKeyPairIndex }

Expand All @@ -194,14 +202,6 @@ public extension DIDCommAgent {
let linkSecretString = String(data: restored.raw, encoding: .utf8)
else { throw EdgeAgentError.cannotFindDIDKeyPairIndex }

let downloader = DownloadDataWithResolver(castor: castor)
guard
let attachment = offer.attachments.first,
let offerFormat = attachment.format
else {
throw PolluxError.unsupportedIssuedMessage
}

let jsonData: Data
switch attachment.data {
case let attchedData as AttachmentBase64:
Expand All @@ -218,7 +218,7 @@ public extension DIDCommAgent {
type: offerFormat,
offerPayload: jsonData,
options: [
.exportableKey(exporting),
.exportableKeys(exporting),
.subjectDID(did),
.linkSecret(id: did.string, secret: linkSecretString),
.credentialDefinitionDownloader(downloader: downloader),
Expand Down
Loading