Skip to content

Digital Signatures

Self provides a secure digital agreement signing workflow that allows users to cryptographically sign documents. This workflow ensures document integrity, non-repudiation, and tamper-proof signatures that can be verified independently. The digital signing process is designed to 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

Try Digital Signatures

Experience digital signatures in action with our demo applications. Each app demonstrates real-world document signing workflows with Self SDK.

Platform Quick Install Source Code
Mobile - Android Download APK View Source
Mobile - iOS Download from App Store View Source
Server - Golang docker run -it ghcr.io/joinself/self-sdk-demo:go View Source
Server - Java docker run -it ghcr.io/joinself/self-sdk-demo:java View Source

Quick Start

  1. Start a server - Use the Docker links above to run a Golang or Java server
  2. Install mobile app - Download the Android or iOS app using the links above
  3. Register Self account - Open the mobile app and follow the registration prompts
  4. Connect to server - Enter the server identifier into the mobile app to establish connection
  5. Test document signing - Follow the prompts in the mobile app to sign a document

Each application includes complete document signing workflows with real cryptographic signatures, verification, and audit trails.

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

The server can send a verification request that contains the document details and signing requirements.

serverAddress := inboxAddress
clientAddress := msg.FromAddress()

pdf := fpdf.New("P", "mm", "A4", "")
pdf.AddPage()
pdf.SetFont("Arial", "B", 16)
pdf.Cell(40, 10, "Document Signing Agreement")
pdf.Ln(20)
pdf.SetFont("Arial", "", 12)
pdf.Cell(0, 10, "This document represents an agreement between:")
pdf.Ln(10)
pdf.Cell(0, 10, "Server: "+serverAddress.String())
pdf.Ln(10)
pdf.Cell(0, 10, "Client: "+clientAddress.String())
pdf.Ln(10)
pdf.Cell(0, 10, "By signing this agreement, both parties acknowledge")
pdf.Ln(10)
pdf.Cell(0, 10, "the terms and conditions of this document signing process.")

agreementBuf := bytes.NewBuffer(make([]byte, 1024))
err := pdf.Output(agreementBuf)
if err != nil {
  log.Fatalf("Failed to generate PDF: %v", err)
}

agreementTerms, err := object.New("application/pdf", agreementBuf.Bytes())
if err != nil {
  log.Fatalf("Failed to create agreement object: %v", err)
}

err = selfAccount.ObjectUpload(agreementTerms, false)
if err != nil {
  log.Fatalf("Failed to upload agreement object: %v", err)
}

claims := map[string]interface{}{
  "termsHash": hex.EncodeToString(agreementTerms.Hash()),
  "parties": []map[string]interface{}{
    {"type": "signatory", "id": serverAddress.String()},
    {"type": "signatory", "id": clientAddress.String()},
  },
}

unsignedAgreementCredential, err := credential.NewCredential().
  CredentialType([]string{"VerifiableCredential", "AgreementCredential"}).
  CredentialSubject(credential.AddressKey(serverAddress)).
  CredentialSubjectClaims(claims).
  CredentialSubjectClaim("terms", hex.EncodeToString(agreementTerms.Id())).
  Issuer(credential.AddressKey(serverAddress)).
  ValidFrom(time.Now()).
  SignWith(serverAddress, time.Now()).
  Finish()

if err != nil {
  log.Fatalf("Failed to create credential: %v", err)
}

signedAgreementCredential, err := selfAccount.CredentialIssue(unsignedAgreementCredential)
if err != nil {
  log.Fatalf("Failed to issue credential: %v", err)
}

unsignedAgreementPresentation, err := credential.NewPresentation().
  PresentationType([]string{"VerifiablePresentation", "AgreementPresentation"}).
  Holder(credential.AddressKey(serverAddress)).
  CredentialAdd(signedAgreementCredential).
  Finish()

if err != nil {
  log.Fatalf("Failed to create presentation: %v", err)
}

signedAgreementPresentation, err := selfAccount.PresentationIssue(unsignedAgreementPresentation)
if err != nil {
  log.Fatalf("Failed to issue presentation: %v", err)
}

content, err := message.NewCredentialVerificationRequest().
  Type([]string{"VerifiableCredential", "AgreementCredential"}).
  Evidence("terms", agreementTerms).
  Proof(signedAgreementPresentation).
  Expires(time.Now().Add(time.Hour * 24)).
  Finish()

if err != nil {
  log.Fatalf("Failed to build verification request: %v", err)
}

err = selfAccount.MessageSend(msg.FromAddress(), content)
if err != nil {
  log.Fatalf("Failed to send document signing request to %s: %v", msg.FromAddress(), err)
} else {
  log.Printf("Successfully sent document signing request to: %s", msg.FromAddress())
}
val terms = "Agreement test"
val agreementTerms = BinaryObject.create(
    "text/plain",
    terms.encodeToByteArray()
)

account.objectStore(agreementTerms)
val uploadStatus = runBlocking {
    suspendCoroutine { continuation ->
        account.objectUpload( agreementTerms, false) { status ->
            continuation.resumeWith(Result.success(status))
        }
    }
}
if (!uploadStatus.success()) {
    println("failed to upload object ${SelfStatusName.getName(uploadStatus.code())}")
    return@configure
}
val claims = HashMap<String, Any>()
claims["termsHash"] = agreementTerms.hash()!!.toHexString()
claims["parties"] = arrayOf(
    hashMapOf("type" to "signatory", "id" to inboxAddress!!.encodeHex()),
    hashMapOf("type" to "signatory", "id" to responderAddress?.encodeHex()),
)

val unsignedAgreementCredential = CredentialBuilder()
    .credentialType(arrayOf("VerifiableCredential", "AgreementCredential"))
    .credentialSubject(Address.key(inboxAddress!!))
    .credentialSubjectClaims(claims)
    .issuer(Address.key(inboxAddress!!))
    .validFrom(Timestamp.now())
    .signWith(inboxAddress!!, Timestamp.now())
    .finish()
val signedAgreementCredential = account.credentialIssue(unsignedAgreementCredential)

val unsignedAgreementPresentation = PresentationBuilder()
    .presentationType(arrayOf("VerifiablePresentation", "AgreementPresentation"))
    .holder(Address.key(inboxAddress!!))
    .credentialAdd(signedAgreementCredential)
    .finish()
val signedAgreementPresentation = account.presentationIssue(unsignedAgreementPresentation)

val agreementRequest = CredentialVerificationRequestBuilder()
    .credentialType(arrayOf("VerifiableCredential", "AgreementCredential"))
    .evidence("terms", agreementTerms)
    .proof(signedAgreementPresentation)
    .expires(Timestamp.now() + 3600)
    .finish()

val sendStatus = account.messageSend(responderAddress!!, agreementRequest)
println("send agreement status:${SelfStatusName.getName(sendStatus.code())} - to:${responderAddress!!.encodeHex()} - requestId:${agreementRequest.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.

// Self SDK: Complete document signing flow (starting from document reception)

// 1. RECEIVE DOCUMENT FROM SERVER
account.setOnRequestListener { msg ->
    when (msg) {
        is VerificationRequest -> {
            if (msg.types().contains(CredentialType.Agreement)) {
                verificationRequest = msg  // Document received and stored
            }
        }
    }
}

// 2. SIGN DOCUMENT (when user approves)
fun signDocument() {
    if (verificationRequest == null) return

    // Self SDK: Create signed response
    val verificationResponse = VerificationResponse.Builder()
        .setRequestId(verificationRequest!!.id())
        .setTypes(verificationRequest!!.types())
        .setToIdentifier(verificationRequest!!.toIdentifier())
        .setFromIdentifier(verificationRequest!!.fromIdentifier())
        .setStatus(ResponseStatus.accepted)  // User approved the document
        .build()

    // 3. RETURN SIGNED DOCUMENT TO SERVER
    account.send(verificationResponse) { messageId, _ ->
        // Document has been cryptographically signed and sent back to server
    }
}
// Self SDK: Complete document signing flow (starting from document reception)

// 1. RECEIVE DOCUMENT FROM SERVER
account.setOnRequestListener { message in
    switch message {
    case is VerificationRequest:
        let verificationRequest = message as! VerificationRequest
        currentVerificationRequest = verificationRequest  // Document received and stored
    }
}

// 2. SIGN DOCUMENT (when user approves)
func signDocument() {
    guard let verificationRequest = currentVerificationRequest else { return }

    // Self SDK: Create signed response
    let verificationResponse = VerificationResponse.Builder()
        .withRequestId(verificationRequest.id())
        .toIdentifier(verificationRequest.toIdentifier())
        .fromIdentifier(verificationRequest.fromIdentifier())
        .withTypes(verificationRequest.types())
        .withStatus(ResponseStatus.accepted)  // User approved the document
        .withCredentials([])  // Optional: additional credentials if needed
        .build()

    // 3. RETURN SIGNED DOCUMENT TO SERVER
    self.sendKMPMessage(message: verificationResponse) { messageId, error in
        // Document has been cryptographically signed and sent back to server
    }
}

// Self SDK: Helper method to send messages
func sendKMPMessage(message: Message, completion: ((_ messageId: String, _ error: Error?) -> ())? = nil) {
    Task(priority: .background, operation: {
        try await self.account.send(message: message, onAcknowledgement: {msgId, error in
            completion?(msgId, error)
        })
    })
}

3. Server: Processing Signature Response

After receiving the verification response, we need to process the signature. This involves validating the response and checking the signature status.

response, err := message.DecodeCredentialVerificationResponse(msg.Content())
if err != nil {
  log.Printf("Failed to decode verification response from %s: %v", msg.FromAddress(), err)
  return
}

switch response.Status() {
case message.ResponseStatusAccepted, message.ResponseStatusCreated:
  log.Printf("Client %s has digitally signed the agreement", msg.FromAddress())
case message.ResponseStatusUnauthorized, message.ResponseStatusForbidden, message.ResponseStatusNotAcceptable:
  log.Printf("Client %s declined to sign the agreement", msg.FromAddress())
default:
  log.Printf("UNKNOWN RESPONSE STATUS from client: %s", msg.FromAddress())
}
ContentType.CREDENTIAL_VERIFICATION_RESPONSE -> {
    val verificationResponse = CredentialVerificationResponse.decode(content)
    println("Response received with status:${verificationResponse.status().name}")
    verificationResponse.credentials().forEach { cred ->
        val claims = cred.credentialSubjectClaims()
        claims.forEach {
            println(
                    "types:${cred.credentialType().toList()}" +
                    "\nfield:${it.key}" +
                    "\nvalue:${it.value}"
            )
            println()
        }
    }
}

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

For further details and advanced use cases, refer to the Essentials section or browse our examples repository for working implementations.