Skip to content
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
184 changes: 184 additions & 0 deletions CERTIFICATE_SECURITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
# Certificate Security Configuration

This document explains the certificate validation configuration for development and production builds.

## Overview

The notification system uses different certificate validation strategies depending on the build type:

- **Debug builds**: Allow self-signed certificates for development
- **Release builds**: Strict certificate validation using system CAs only

## Build-Specific Configuration

### Debug Builds (Development)

**File**: `demo-app/app/src/debug/res/xml/network_security_config.xml`

- Allows self-signed certificates for localhost, 127.0.0.1, and 10.0.2.2 (Android emulator)
- Trusts both system CAs and user-added certificates
- Includes debug overrides for development flexibility
- **WARNING**: Never use debug configuration in production

### Release Builds (Production)

**File**: `demo-app/app/src/release/res/xml/network_security_config.xml`

- Strict certificate validation using system CAs only
- No user certificate trust for maximum security
- Requires proper CA-signed certificates
- Production domain placeholder needs to be updated

## Android App Implementation

### HTTP Client Creation

The `MainActivity.createHttpClient()` method automatically selects the appropriate client:

```kotlin
private fun createHttpClient(): OkHttpClient {
return if (BuildConfig.DEBUG) {
createDebugHttpClient() // Allows self-signed certs
} else {
createReleaseHttpClient() // Strict validation
}
}
```

### Debug Client Features

- Logs certificate acceptance for debugging
- Accepts self-signed certificates
- Flexible hostname verification
- Development-friendly error handling

### Release Client Features

- Uses system certificate authorities only
- Respects network security configuration
- Strict hostname verification
- Production-grade security

## Production Deployment Requirements

### 1. Backend Certificate Setup

For production deployment, replace the self-signed certificate in `app-backend/` with proper CA-signed certificates:

```bash
# Replace these files with CA-signed certificates:
app-backend/cert.pem # Public certificate
app-backend/key.pem # Private key
```

### 2. Certificate Options

**Option A: Let's Encrypt (Free)**
```bash
certbot certonly --standalone -d your-domain.com
cp /etc/letsencrypt/live/your-domain.com/fullchain.pem app-backend/cert.pem
cp /etc/letsencrypt/live/your-domain.com/privkey.pem app-backend/key.pem
```

**Option B: Commercial CA**
- Purchase certificate from trusted CA (DigiCert, Comodo, etc.)
- Follow CA's installation instructions
- Place certificate files in `app-backend/`

**Option C: Internal CA (Enterprise)**
- Use organizational certificate authority
- Ensure certificates are trusted by target devices
- Consider certificate pinning for additional security

### 3. Update Production Domain

Edit `demo-app/app/src/release/res/xml/network_security_config.xml`:

```xml
<domain includeSubdomains="true">your-actual-domain.com</domain>
```

### 4. Android App Configuration

Update the default backend URL in `SettingsActivity` to use the production domain:

```kotlin
const val DEFAULT_BACKEND_URL = "https://your-production-domain.com:8443"
```

## Security Verification

### Testing Debug Build

1. Build debug APK: `./gradlew assembleDebug`
2. Install on test device
3. Verify self-signed certificate acceptance
4. Check logs for certificate debugging messages

### Testing Release Build

1. Build release APK: `./gradlew assembleRelease`
2. Install on test device
3. Verify rejection of self-signed certificates
4. Confirm only CA-signed certificates are accepted

### Certificate Validation Test

```bash
# Test certificate chain
openssl s_client -connect your-domain.com:8443 -showcerts

# Verify certificate expiration
openssl x509 -in app-backend/cert.pem -text -noout | grep "Not After"
```

## Security Best Practices

1. **Never deploy debug builds to production**
2. **Use HTTPS everywhere** - no HTTP in production
3. **Monitor certificate expiration** - set up renewal alerts
4. **Consider certificate pinning** for high-security applications
5. **Regular security audits** of certificate configuration
6. **Backup certificate private keys** securely

## Troubleshooting

### Common Issues

**Certificate not trusted in release build**
- Verify certificate is signed by trusted CA
- Check network_security_config.xml domain configuration
- Ensure certificate matches domain name

**Debug build not accepting self-signed certificate**
- Verify debug network_security_config.xml exists
- Check build type configuration
- Review OkHttp client implementation

**Production deployment fails**
- Verify certificate and private key match
- Check certificate expiration date
- Ensure proper file permissions on server

### Logging

The app provides detailed logging for certificate validation:

- Debug builds: Certificate acceptance logged
- Release builds: Strict validation logged
- Network errors: Detailed error messages

## Monitoring

For production deployments, monitor:

- Certificate expiration dates
- SSL/TLS handshake failures
- Certificate validation errors
- Client connection success rates

---

**Last Updated**: January 2025
**Security Level**: Production Ready
**Next Review**: Certificate expiration check
2 changes: 1 addition & 1 deletion app-backend/go.mod
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module app-backend

go 1.24.3
go 1.24.5
107 changes: 104 additions & 3 deletions app-backend/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"log"
"net/http"
"os"
"strings"
"sync"
"time"
)
Expand Down Expand Up @@ -77,11 +78,111 @@ func (ts *TokenStore) Count() int {
return len(ts.tokenIDs)
}

// RequestLog represents a structured log entry for HTTP requests
type RequestLog struct {
Timestamp time.Time `json:"timestamp"`
Method string `json:"method"`
Path string `json:"path"`
RemoteAddr string `json:"remote_addr"`
UserAgent string `json:"user_agent"`
StatusCode int `json:"status_code"`
ResponseTime int64 `json:"response_time_ms"`
BodySize int64 `json:"body_size"`
Error string `json:"error,omitempty"`
}

// ResponseWriter wrapper to capture status code and response size
type loggingResponseWriter struct {
http.ResponseWriter
statusCode int
bodySize int64
}

func (lrw *loggingResponseWriter) WriteHeader(code int) {
lrw.statusCode = code
lrw.ResponseWriter.WriteHeader(code)
}

func (lrw *loggingResponseWriter) Write(b []byte) (int, error) {
size, err := lrw.ResponseWriter.Write(b)
lrw.bodySize += int64(size)
return size, err
}

var (
tokenStore = NewTokenStore()
publicKeyHash string
)

// loggingMiddleware wraps HTTP handlers to provide structured logging
func loggingMiddleware(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
start := time.Now()

// Create logging response writer
lrw := &loggingResponseWriter{
ResponseWriter: w,
statusCode: 200, // Default status code
}

// Call the next handler
next(lrw, r)

// Calculate response time
responseTime := time.Since(start).Milliseconds()

// Create structured log entry
logEntry := RequestLog{
Timestamp: start,
Method: r.Method,
Path: r.URL.Path,
RemoteAddr: getClientIP(r),
UserAgent: r.UserAgent(),
StatusCode: lrw.statusCode,
ResponseTime: responseTime,
BodySize: lrw.bodySize,
}

// Add error field for non-2xx responses
if lrw.statusCode >= 400 {
logEntry.Error = http.StatusText(lrw.statusCode)
}

// Log as JSON
logJSON, err := json.Marshal(logEntry)
if err != nil {
log.Printf("Error marshaling log entry: %v", err)
return
}

log.Printf("REQUEST_LOG: %s", string(logJSON))
}
}

// getClientIP extracts the real client IP from request headers
func getClientIP(r *http.Request) string {
// Check X-Forwarded-For header (for proxies/load balancers)
if xForwardedFor := r.Header.Get("X-Forwarded-For"); xForwardedFor != "" {
// X-Forwarded-For can contain multiple IPs, take the first one
ifs := strings.Split(xForwardedFor, ",")
if len(ifs) > 0 {
return strings.TrimSpace(ifs[0])
}
}

// Check X-Real-IP header (for nginx)
if xRealIP := r.Header.Get("X-Real-IP"); xRealIP != "" {
return xRealIP
}

// Fall back to RemoteAddr
// Remove port if present
if idx := strings.LastIndex(r.RemoteAddr, ":"); idx != -1 {
return r.RemoteAddr[:idx]
}
return r.RemoteAddr
}

func main() {
flag.Parse()

Expand All @@ -101,9 +202,9 @@ func main() {
publicKeyHash = computePublicKeyHash(publicKeyPEM)
log.Printf("Public key hash computed: %s", publicKeyHash[:16]+"...")

http.HandleFunc("/register", handleRegister)
http.HandleFunc("/send-all", handleSendAll)
http.HandleFunc("/", handleHome)
http.HandleFunc("/register", loggingMiddleware(handleRegister))
http.HandleFunc("/send-all", loggingMiddleware(handleSendAll))
http.HandleFunc("/", loggingMiddleware(handleHome))

log.Printf("App Backend Server starting on HTTPS port %s", *port)
log.Printf("Web interface available at: https://localhost:%s", *port)
Expand Down
6 changes: 3 additions & 3 deletions demo-app/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,11 @@ android {
}

compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = '1.8'
jvmTarget = '11'
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<!-- DEBUG BUILD: Allow self-signed certificates for development -->
<domain-config cleartextTrafficPermitted="false">
<domain includeSubdomains="true">10.0.2.2</domain>
<domain includeSubdomains="true">localhost</domain>
<domain includeSubdomains="true">127.0.0.1</domain>
<trust-anchors>
<!-- Trust user added CAs while debuggable only -->
<!-- Trust user added CAs (including self-signed) in debug builds -->
<certificates src="user"/>
<!-- Trust system CAs -->
<certificates src="system"/>
Expand All @@ -17,4 +20,12 @@
<certificates src="user"/>
</trust-anchors>
</base-config>
</network-security-config>

<!-- Debug configuration: allows self-signed certificates -->
<debug-overrides>
<trust-anchors>
<certificates src="system"/>
<certificates src="user"/>
</trust-anchors>
</debug-overrides>
</network-security-config>
Loading
Loading