// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package webrtc import ( "crypto" "crypto/ecdsa" "crypto/rand" "crypto/rsa" "crypto/x509" "crypto/x509/pkix" "encoding/base64" "encoding/pem" "fmt" "math/big" "strings" "time" "github.com/pion/dtls/v3/pkg/crypto/fingerprint" "github.com/pion/webrtc/v4/pkg/rtcerr" ) // Certificate represents a x509Cert used to authenticate WebRTC communications. type Certificate struct { privateKey crypto.PrivateKey x509Cert *x509.Certificate statsID string } // NewCertificate generates a new x509 compliant Certificate to be used // by DTLS for encrypting data sent over the wire. This method differs from // GenerateCertificate by allowing to specify a template x509.Certificate to // be used in order to define certificate parameters. func NewCertificate(key crypto.PrivateKey, tpl x509.Certificate) (*Certificate, error) { var err error var certDER []byte switch sk := key.(type) { case *rsa.PrivateKey: pk := sk.Public() tpl.SignatureAlgorithm = x509.SHA256WithRSA certDER, err = x509.CreateCertificate(rand.Reader, &tpl, &tpl, pk, sk) if err != nil { return nil, &rtcerr.UnknownError{Err: err} } case *ecdsa.PrivateKey: pk := sk.Public() tpl.SignatureAlgorithm = x509.ECDSAWithSHA256 certDER, err = x509.CreateCertificate(rand.Reader, &tpl, &tpl, pk, sk) if err != nil { return nil, &rtcerr.UnknownError{Err: err} } default: return nil, &rtcerr.NotSupportedError{Err: ErrPrivateKeyType} } cert, err := x509.ParseCertificate(certDER) if err != nil { return nil, &rtcerr.UnknownError{Err: err} } return &Certificate{ privateKey: key, x509Cert: cert, statsID: fmt.Sprintf("certificate-%d", time.Now().UnixNano()), }, nil } // Equals determines if two certificates are identical by comparing both the // secretKeys and x509Certificates. func (c Certificate) Equals(cert Certificate) bool { switch cSK := c.privateKey.(type) { case *rsa.PrivateKey: if oSK, ok := cert.privateKey.(*rsa.PrivateKey); ok { if cSK.N.Cmp(oSK.N) != 0 { return false } return c.x509Cert.Equal(cert.x509Cert) } return false case *ecdsa.PrivateKey: if oSK, ok := cert.privateKey.(*ecdsa.PrivateKey); ok { if cSK.X.Cmp(oSK.X) != 0 || cSK.Y.Cmp(oSK.Y) != 0 { return false } return c.x509Cert.Equal(cert.x509Cert) } return false default: return false } } // Expires returns the timestamp after which this certificate is no longer valid. func (c Certificate) Expires() time.Time { if c.x509Cert == nil { return time.Time{} } return c.x509Cert.NotAfter } // GetFingerprints returns the list of certificate fingerprints, one of which // is computed with the digest algorithm used in the certificate signature. func (c Certificate) GetFingerprints() ([]DTLSFingerprint, error) { fingerprintAlgorithms := []crypto.Hash{crypto.SHA256} res := make([]DTLSFingerprint, len(fingerprintAlgorithms)) i := 0 for _, algo := range fingerprintAlgorithms { name, err := fingerprint.StringFromHash(algo) if err != nil { // nolint return nil, fmt.Errorf("%w: %v", ErrFailedToGenerateCertificateFingerprint, err) } value, err := fingerprint.Fingerprint(c.x509Cert, algo) if err != nil { // nolint return nil, fmt.Errorf("%w: %v", ErrFailedToGenerateCertificateFingerprint, err) } res[i] = DTLSFingerprint{ Algorithm: name, Value: value, } } return res[:i+1], nil } // GenerateCertificate causes the creation of an X.509 certificate and // corresponding private key. func GenerateCertificate(secretKey crypto.PrivateKey) (*Certificate, error) { // Max random value, a 130-bits integer, i.e 2^130 - 1 maxBigInt := new(big.Int) /* #nosec */ maxBigInt.Exp(big.NewInt(2), big.NewInt(130), nil).Sub(maxBigInt, big.NewInt(1)) /* #nosec */ serialNumber, err := rand.Int(rand.Reader, maxBigInt) if err != nil { return nil, &rtcerr.UnknownError{Err: err} } return NewCertificate(secretKey, x509.Certificate{ Issuer: pkix.Name{CommonName: generatedCertificateOrigin}, NotBefore: time.Now().AddDate(0, 0, -1), NotAfter: time.Now().AddDate(0, 1, -1), SerialNumber: serialNumber, Version: 2, Subject: pkix.Name{CommonName: generatedCertificateOrigin}, }) } // CertificateFromX509 creates a new WebRTC Certificate from a given PrivateKey and Certificate // // This can be used if you want to share a certificate across multiple PeerConnections. func CertificateFromX509(privateKey crypto.PrivateKey, certificate *x509.Certificate) Certificate { return Certificate{privateKey, certificate, fmt.Sprintf("certificate-%d", time.Now().UnixNano())} } func (c Certificate) collectStats(report *statsReportCollector) error { report.Collecting() fingerPrintAlgo, err := c.GetFingerprints() if err != nil { return err } base64Certificate := base64.RawURLEncoding.EncodeToString(c.x509Cert.Raw) stats := CertificateStats{ Timestamp: statsTimestampFrom(time.Now()), Type: StatsTypeCertificate, ID: c.statsID, Fingerprint: fingerPrintAlgo[0].Value, FingerprintAlgorithm: fingerPrintAlgo[0].Algorithm, Base64Certificate: base64Certificate, IssuerCertificateID: c.x509Cert.Issuer.String(), } report.Collect(stats.ID, stats) return nil } // CertificateFromPEM creates a fresh certificate based on a string containing // pem blocks fort the private key and x509 certificate. func CertificateFromPEM(pems string) (*Certificate, error) { //nolint: cyclop var cert *x509.Certificate var privateKey crypto.PrivateKey var block *pem.Block more := []byte(pems) for { var err error block, more = pem.Decode(more) if block == nil { break } // decode & parse the certificate switch block.Type { case "CERTIFICATE": if cert != nil { return nil, errCertificatePEMMultipleCert } cert, err = x509.ParseCertificate(block.Bytes) // If parsing failed using block.Bytes, then parse the bytes as base64 and try again if err != nil { var n int certBytes := make([]byte, base64.StdEncoding.DecodedLen(len(block.Bytes))) n, err = base64.StdEncoding.Decode(certBytes, block.Bytes) if err == nil { cert, err = x509.ParseCertificate(certBytes[:n]) } } case "PRIVATE KEY": if privateKey != nil { return nil, errCertificatePEMMultiplePriv } privateKey, err = x509.ParsePKCS8PrivateKey(block.Bytes) } // Report errors from parsing either the private key or the certificate if err != nil { return nil, fmt.Errorf("failed to decode %s: %w", block.Type, err) } } if cert == nil || privateKey == nil { return nil, errCertificatePEMMissing } ret := CertificateFromX509(privateKey, cert) return &ret, nil } // PEM returns the certificate encoded as two pem block: once for the X509 // certificate and the other for the private key. func (c Certificate) PEM() (string, error) { // First write the X509 certificate var builder strings.Builder xcertBytes := make( []byte, base64.StdEncoding.EncodedLen(len(c.x509Cert.Raw))) base64.StdEncoding.Encode(xcertBytes, c.x509Cert.Raw) err := pem.Encode(&builder, &pem.Block{Type: "CERTIFICATE", Bytes: xcertBytes}) if err != nil { return "", fmt.Errorf("failed to pem encode the X certificate: %w", err) } // Next write the private key privBytes, err := x509.MarshalPKCS8PrivateKey(c.privateKey) if err != nil { return "", fmt.Errorf("failed to marshal private key: %w", err) } err = pem.Encode(&builder, &pem.Block{Type: "PRIVATE KEY", Bytes: privBytes}) if err != nil { return "", fmt.Errorf("failed to encode private key: %w", err) } return builder.String(), nil }