Skip to content

Digital Agreement Signing

Self provides a secure digital agreement signing workflow that allows users to cryptographically sign documents using their digital identity. This workflow ensures document integrity, non-repudiation, and tamper-proof signatures that can be verified independently. The digital signing process is designed to be scalable and can be used in a variety of scenarios, such as:

  • Legal contract signing for business applications
  • Terms of service acceptance for web and mobile platforms
  • Document approval workflows in enterprise systems
  • Regulatory compliance documentation
  • Financial agreement signing
  • Healthcare consent forms

How to build a digital agreement signing workflow

You can build a secure digital agreement signing workflow with Self using verification requests and responses. Let's break down the process into steps:

1. Server: Verification Request

Once we have the user's address (userAddress) and the document to be signed, we can send a verification request. This request contains the document details and signing requirements. For more details on verification requests, check out the Verification guide.

// Create the document to be signed
documentHash := "sha256:a1b2c3d4e5f6..." // SHA-256 hash of the document
documentContent := "Terms of Service Agreement v2.1..."

content, err := message.NewVerificationRequest().
  Type([]string{"VerifiablePresentation", "DocumentSigning"}).
  Object(documentHash).
  Subject("document_signature").
  Description("Please review and sign this Terms of Service Agreement").
  Attachment([]byte(documentContent)).
  Finish()

if err != nil {
  log.Fatal("failed to encode verification request message", "error", err)
}

err = selfAccount.MessageSend(userAddress, content)
if err != nil {
  log.Fatal("failed to send verification request message", "error", err)
}
val documentHash = "sha256:a1b2c3d4e5f6..." // SHA-256 hash of the document
val documentContent = "Terms of Service Agreement v2.1..."

val content = NewVerificationRequest()
  .presentationType(arrayOf("VerifiablePresentation", "DocumentSigning"))
  .obj(documentHash)
  .subject("document_signature")
  .description("Please review and sign this Terms of Service Agreement")
  .attachment(documentContent.toByteArray())
  .finish()

val sendStatus = account.messageSend(userAddress, content)
println("send VerificationRequest status: ${sendStatus.code()} - requestId:${verificationRequest.id().toHexString()}")

2. Client: Handle Verification Request

You need to listen to the verification request in the account's callback and then check the incoming message for its type and details. If the request is for document signing, display the document content to the user for review and signature.

var verificationRequest: VerificationRequest? = null 
account.setOnRequestListener { msg ->
      when (msg) {
          is VerificationRequest -> {
              if (msg.types().contains("DocumentSigning") && msg.subject() == "document_signature") {                    
                  verificationRequest = msg

                  // Extract document content from the request
                  val documentContent = String(msg.attachment())
                  val documentHash = msg.obj()

                  // Navigate to document signing UI
                  navController.navigate("documentSigningRoute") 
              }
          }
      }
  }

  // Integrate document signing UI flow into Navigation
  addDocumentSigningRoute(navController, route = "documentSigningRoute", selfModifier = selfModifier, 
      document = { documentContent },
      onFinish = { accepted ->
          if (verificationRequest != null) {
              // Send the verification response after user decision
              sendVerificationResponse(accepted)               
          }            
      }
  )

  fun sendVerificationResponse(accepted: Boolean) {       
      val status = if (accepted) ResponseStatus.accepted else ResponseStatus.rejected

      val verificationResponse = VerificationResponse.Builder()
          .setRequestId(verificationRequest.id())
          .setTypes(verificationRequest.types())
          .setToIdentifier(verificationRequest.toIdentifier())
          .setFromIdentifier(verificationRequest.fromIdentifier())
          .setStatus(status)
          .setObject(verificationRequest.obj()) // Include document hash
          .setSubject(verificationRequest.subject())
          .build()

      account.send(verificationResponse)       
  }
// 1. Handle verification request in your request listener
account.setOnRequestListener { [weak self] requestMessage in
    guard let self = self else { return }

    if let verificationRequest = requestMessage as? VerificationRequest {
        print("Verification request received from: \(verificationRequest.fromIdentifier())")

        Task { @MainActor in
            await self.handleVerificationRequest(verificationRequest)
        }
    }
}

// 2. Handle verification request with proper filtering and thread safety
private func handleVerificationRequest(_ verificationRequest: VerificationRequest) async {
    let types = verificationRequest.types()
    let subject = verificationRequest.subject()

    print("Verification request types: \(types)")
    print("Verification request subject: \(subject)")

    // Check if this is a document signing request
    if types.contains("DocumentSigning") && subject == "document_signature" {
        print("✅ Processing document signing request")
        await handleDocumentSigning(verificationRequest)
    } else {
        print("❌ Unsupported verification request type")
    }
}

// 3. Handle document signing with proper thread management
private func handleDocumentSigning(_ verificationRequest: VerificationRequest) async {
    // Extract document details
    let documentHash = verificationRequest.obj()
    let documentDescription = verificationRequest.description()

    // Extract document content if available
    var documentContent: String = ""
    if let attachment = verificationRequest.attachment() {
        documentContent = String(data: attachment, encoding: .utf8) ?? ""
    }

    print("Document to sign:")
    print("Hash: \(documentHash)")
    print("Description: \(documentDescription)")
    print("Content preview: \(String(documentContent.prefix(100)))...")

    // Store the request for later response
    currentVerificationRequest = verificationRequest

    // Navigate to document signing UI on main thread
    await MainActor.run {
        // Update UI to show document signing screen
        currentScreen = .documentSigning
    }
}

// 4. Send verification response after user makes signing decision
private func sendVerificationResponse(
    to request: VerificationRequest,
    accepted: Bool
) async {
    let status = accepted ? ResponseStatus.accepted : ResponseStatus.rejected

    do {
        let response = VerificationResponse.Builder()
            .withRequestId(request.id())
            .withTypes(request.types())
            .toIdentifier(request.toIdentifier())
            .fromIdentifier(request.fromIdentifier())
            .withStatus(status)
            .withObject(request.obj()) // Include document hash
            .withSubject(request.subject())
            .build()

        try await account.send(message: response, onAcknowledgement: { messageId, error in
            if let error = error {
                print("❌ Verification response failed: \(error)")
            } else {
                print("✅ Document signing response sent successfully")
            }
        })
    } catch {
        print("Error building verification response: \(error)")
    }
}

// 5. User interface methods for accepting/rejecting signature
private func acceptDocumentSigning() {
    guard let request = currentVerificationRequest else { return }

    Task {
        await sendVerificationResponse(to: request, accepted: true)
        await MainActor.run {
            currentScreen = .signatureResult(success: true)
        }
    }
}

private func rejectDocumentSigning() {
    guard let request = currentVerificationRequest else { return }

    Task {
        await sendVerificationResponse(to: request, accepted: false)
        await MainActor.run {
            currentScreen = .signatureResult(success: false)
        }
    }
}

See full examples

3. Server: Processing Signature Response

After receiving the verification response, we need to process the signature. This involves validating the response, checking the signature status, and storing the signed document record. For more information on verification validation, see the Verification Validation guide.

// Process the verification response
if response.Status() == message.ResponseStatusAccepted {
  log.Printf("INFO: Document signed successfully by %s", response.FromIdentifier())

  // Validate the response signature
  err = response.Validate()
  if err != nil {
    log.Printf("WARN: failed to validate verification response - error: %v", err)
    return
  }

  // Extract signature details
  documentHash := response.Object()
  subject := response.Subject()
  signerID := response.FromIdentifier()
  signedAt := response.CreatedAt()

  // Store the signature record
  signatureRecord := SignatureRecord{
    DocumentHash: documentHash,
    SignerID:     signerID,
    SignedAt:     signedAt,
    Status:       "signed",
    RequestID:    response.RequestID(),
  }

  err = storeSignatureRecord(signatureRecord)
  if err != nil {
    log.Printf("ERROR: failed to store signature record - error: %v", err)
  } else {
    log.Printf("INFO: Signature record stored successfully for document %s", documentHash)
  }

} else if response.Status() == message.ResponseStatusRejected {
  log.Printf("INFO: Document signing rejected by %s", response.FromIdentifier())

  // Store rejection record
  rejectionRecord := SignatureRecord{
    DocumentHash: response.Object(),
    SignerID:     response.FromIdentifier(),
    SignedAt:     response.CreatedAt(),
    Status:       "rejected",
    RequestID:    response.RequestID(),
  }

  err = storeSignatureRecord(rejectionRecord)
  if err != nil {
    log.Printf("ERROR: failed to store rejection record - error: %v", err)
  }
}
// Process the verification response
when (response.status()) {
  ResponseStatus.accepted -> {
    println("Document signed successfully by ${response.fromIdentifier()}")

    // Validate the response signature
    response.validate()

    // Extract signature details
    val documentHash = response.obj()
    val subject = response.subject()
    val signerID = response.fromIdentifier()
    val signedAt = response.createdAt()

    // Store the signature record
    val signatureRecord = SignatureRecord(
      documentHash = documentHash,
      signerID = signerID,
      signedAt = signedAt,
      status = "signed",
      requestID = response.requestId()
    )

    storeSignatureRecord(signatureRecord)
    println("Signature record stored successfully for document $documentHash")
  }

  ResponseStatus.rejected -> {
    println("Document signing rejected by ${response.fromIdentifier()}")

    // Store rejection record
    val rejectionRecord = SignatureRecord(
      documentHash = response.obj(),
      signerID = response.fromIdentifier(),
      signedAt = response.createdAt(),
      status = "rejected",
      requestID = response.requestId()
    )

    storeSignatureRecord(rejectionRecord)
  }
}

4. Document Integrity and Audit Trail

The digital signature workflow creates a comprehensive audit trail that can be used for legal and compliance purposes:

type SignatureRecord struct {
  DocumentHash  string    `json:"document_hash"`
  SignerID      string    `json:"signer_id"`
  SignedAt      time.Time `json:"signed_at"`
  Status        string    `json:"status"` // "signed", "rejected", "pending"
  RequestID     string    `json:"request_id"`
  IPAddress     string    `json:"ip_address,omitempty"`
  UserAgent     string    `json:"user_agent,omitempty"`
  Geolocation   string    `json:"geolocation,omitempty"`
}

func VerifyDocumentSignature(documentHash, signerID string) (*SignatureRecord, error) {
  // Retrieve signature record from database
  record, err := getSignatureRecord(documentHash, signerID)
  if err != nil {
    return nil, fmt.Errorf("signature record not found: %w", err)
  }

  // Verify the signature is still valid
  if record.Status != "signed" {
    return nil, fmt.Errorf("document not signed or signature rejected")
  }

  // Additional integrity checks
  if time.Since(record.SignedAt) > time.Hour*24*365 { // 1 year expiry
    log.Printf("WARN: signature is older than 1 year")
  }

  return record, nil
}
data class SignatureRecord(
  val documentHash: String,
  val signerID: String,
  val signedAt: Date,
  val status: String, // "signed", "rejected", "pending"
  val requestID: String,
  val ipAddress: String? = null,
  val userAgent: String? = null,
  val geolocation: String? = null
)

fun verifyDocumentSignature(documentHash: String, signerID: String): SignatureRecord? {
  // Retrieve signature record from database
  val record = getSignatureRecord(documentHash, signerID) 
    ?: throw IllegalArgumentException("Signature record not found")

  // Verify the signature is still valid
  if (record.status != "signed") {
    throw IllegalStateException("Document not signed or signature rejected")
  }

  // Additional integrity checks
  val daysSinceSigning = (Date().time - record.signedAt.time) / (1000 * 60 * 60 * 24)
  if (daysSinceSigning > 365) { // 1 year expiry
    println("WARN: signature is older than 1 year")
  }

  return record
}

Security Considerations

When implementing digital agreement signing, consider these important security aspects:

Document Integrity

  • Always use cryptographic hashes (SHA-256 or stronger) to ensure document integrity
  • Store the original document hash with the signature record
  • Verify document hash before displaying to users

Non-Repudiation

  • Each signature is cryptographically linked to the signer's Self identity
  • Signature records include timestamps and metadata for audit trails
  • Digital signatures cannot be forged or denied by the signer
  • Self's digital signatures meet legal standards for electronic signatures in many jurisdictions
  • Maintain comprehensive audit logs for compliance requirements
  • Consider jurisdiction-specific requirements for digital signature validity

Best Practices

  • Implement signature expiration policies based on your use case
  • Store signature records in immutable audit logs
  • Provide clear consent mechanisms for users
  • Include document version control in your signing workflow

Conclusion

This example demonstrates how to build a comprehensive digital agreement signing workflow with Self. We've seen how to send verification requests for document signing, handle the signing process on mobile clients, validate signature responses, and maintain audit trails for compliance.

The Self SDK provides a secure, legally-compliant foundation for digital document signing that ensures: - Identity verification: Signers are authenticated using their Self digital identity - Document integrity: Cryptographic hashes prevent document tampering - Non-repudiation: Signatures cannot be denied or forged - Audit compliance: Comprehensive logging for regulatory requirements

💻 Complete Working Examples

For complete, runnable implementations that you can test immediately, explore our official examples repository:

🚀 Self SDK Examples Repository

Quick Start:

# Clone with all submodules
git clone --recurse-submodules https://github.com/joinself/self-sdk-examples.git

Platform-Specific Examples: - Android: android/SelfExamples/verification/ - Complete verification and document signing flow - iOS: ios/Example/ - Native iOS verification response implementation - Golang: golang/examples/ - Server-side verification request handling - Java: java/ - Enterprise server implementations

Key Features Demonstrated: - Document Hashing: Cryptographic document integrity verification - Signature Workflows: Complete request/response patterns for document signing - Audit Trails: Comprehensive logging and signature record management - Legal Compliance: Best practices for electronic signature validity

By leveraging these Self features, you can create a robust, secure, and legally-compliant digital signing system that streamlines agreement processes while maintaining the highest security standards.